Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

626 lines
23 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"encoding/binary"
"strings"
"sync/atomic"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
)
func (s *Service) handleGameFrame(session *gameSession, frame []byte) error {
records, err := dnfproto.ParseLatestGameTCPRecords(frame)
if err != nil {
return err
}
for _, record := range records {
runtimeName := runtimeCmdPacketName(record.GameHeader.Cmd, record.GameHeader.Type)
runtimeKnown := runtimeCmdPacketKnown(record.GameHeader.Cmd, record.GameHeader.Type)
s.logGamePacket(session, "RECV", "game", frame,
"cmd", record.GameHeader.Cmd,
"type", record.GameHeader.Type,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"body_len", len(record.Body))
s.logPacketEvent("game-transport-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"cmd", record.GameHeader.Cmd,
"type", record.GameHeader.Type,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"body_len", len(record.Body),
"frame_len", len(frame))
if err := s.handleGameCommand(session, record.GameHeader.Cmd, record.GameHeader.Type, record.Body); err != nil {
return err
}
}
return nil
}
func (s *Service) sendGame(session *gameSession, cmd byte, typ uint16, body []byte) error {
return s.sendGameWithTransport(session, cmd, typ, body, dnfproto.TransportOptions{})
}
// sendGameFixed15Route is reserved for the legacy GET_USERINFO character
// roster response. That exchange is received with the old game header but
// its S2C NOTI is read by the fixed-15 server-header path; wrapping it again
// in the 12+9 latest transport shifts the route and business body boundaries.
func (s *Service) sendGameFixed15Route(session *gameSession, cmd byte, typ uint16, body []byte, route byte) error {
return s.withGameWire(session, func() error {
frame, err := dnfproto.BuildFixed15GameServerPacket(cmd, typ, body, route)
if err != nil {
return err
}
sequence := session.sequence
session.sequence++
s.logPacketEvent("game-fixed15-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"cmd", cmd,
"type", typ,
"sequence", sequence,
"route", route,
"body_len", len(body),
"frame_len", len(frame))
s.logGamePacket(session, "SEND", "game-fixed15", frame,
"cmd", cmd,
"type", typ,
"header_seq", sequence,
"body_len", len(body))
return s.writeGameRawPacketLocked(session, frame)
})
}
func (s *Service) sendGameWithTransport(session *gameSession, cmd byte, typ uint16, body []byte, options dnfproto.TransportOptions) error {
return s.withGameWire(session, func() error {
sequence := session.sequence
options.Sequence = sequence
options.OuterValue8 = s.options.gameOuterToken
frame, err := dnfproto.BuildLatestGameTCP(cmd, typ, body, dnfproto.TransportOptions{
Sequence: options.Sequence,
Route: options.Route,
Flag: options.Flag,
OuterKind: options.OuterKind,
OuterValue4: options.OuterValue4,
OuterValue8: options.OuterValue8,
})
if err != nil {
return err
}
session.sequence++
s.logPacketEvent("game-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"cmd", cmd,
"type", typ,
"sequence", sequence,
"route", options.Route,
"flag", options.Flag,
"outer_value4", options.OuterValue4,
"outer_value8", options.OuterValue8,
"body_len", len(body),
"frame_len", len(frame))
s.logGamePacket(session, "SEND", "game", frame,
"cmd", cmd,
"type", typ,
"header_seq", sequence,
"body_len", len(body))
return s.writeGameRawPacketLocked(session, frame)
})
}
func historicalUpperBodyCodec(bodyCodec string) bool {
normalized := strings.ToLower(strings.TrimSpace(bodyCodec))
for _, forbidden := range []string{"dove", "fixture", "capture", "replay", "pcap", "reference_s2c"} {
if strings.Contains(normalized, forbidden) {
return true
}
}
return false
}
func (s *Service) sendGameUpperFixed16Transport(session *gameSession, msgID uint16, body []byte, classification byte, marker uint32, bodyEncoded bool, bodyCodec string) error {
if historicalUpperBodyCodec(bodyCodec) {
s.logGameEvent(session, "game-upper-historical-body-blocked",
"msg_id", msgID,
"classification", classification,
"body_len", len(body),
"body_codec", bodyCodec,
"reason", "captured_or_dove_derived_body_is_not_a_production_packet_source")
return nil
}
const headerSize = dnfproto.GameServerUpperHeaderSize16
if bodyCodec == "" {
bodyCodec = "current_fixed16_plain"
}
packet := make([]byte, headerSize+len(body))
packet[0] = classification
binary.LittleEndian.PutUint16(packet[1:3], msgID)
binary.LittleEndian.PutUint32(packet[3:7], uint32(len(packet)))
binary.LittleEndian.PutUint32(packet[7:11], marker)
// MCP/IDA sub_2261CB0: 16 字节 upper 头的第 15 偏移低位才是 zlib/保护层展开标志。
// 这里仍保留 header[7..10] 的历史 marker 日志字段,但真实触发客户端展开必须写 packet[15]。
packet[15] = byte(marker)
copy(packet[headerSize:], body)
runtimeName := upperRuntimeCmdPacketName(msgID, classification)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, classification)
s.logPacketEvent("game-upper-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"sequence", 0,
"header_size", headerSize,
"header_marker", marker,
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec,
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"header_seq", 0,
"header_size", headerSize,
"header_marker", marker,
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec)
return s.writeGameUpperPacket(session, packet)
}
func (s *Service) sendGameUpperRaw(session *gameSession, msgID uint16, body []byte) error {
return s.sendGameUpperRawClass(session, msgID, body, dnfproto.DefaultChannelClassification)
}
func (s *Service) sendGameUpperSecondaryRaw(session *gameSession, msgID uint16, body []byte) error {
return s.sendGameUpperRawClass(session, msgID, body, 0)
}
func (s *Service) sendGameUpperCharacterRosterRaw(session *gameSession, body []byte) error {
return s.withGameWire(session, func() error {
const classification byte = 0
msgID := uint16(2)
sequence := session.upperSeq
headerSize := s.gameUpperHeaderSize()
wireBody := append([]byte(nil), body...)
bodyEncoded := false
bodyCodec := gameUpperBodyCodecPlain
// S2C 回包二次加密:与另一条发送路径同一处插入(加密先于 checksum)。
if s.applyGameS2CBodyCipher(session, wireBody, msgID, sequence) {
bodyEncoded = true
bodyCodec = dnfproto.S2CCipherCodecName
}
packet, err := dnfproto.BuildGameServerUpperPacketWithHeaderSize(msgID, wireBody, sequence, classification, headerSize)
if err != nil {
return err
}
session.upperSeq++
runtimeName := upperRuntimeCmdPacketName(msgID, classification)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, classification)
s.logPacketEvent("game-upper-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"sequence", sequence,
"header_size", headerSize,
"plain_body_len", len(body),
"body_len", len(wireBody),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec,
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"header_seq", sequence,
"header_size", headerSize,
"plain_body_len", len(body),
"body_len", len(wireBody),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec)
if err := s.writeGameUpperPacketLocked(session, packet); err != nil {
return err
}
noteEmptyRosterSlotProbe(session, body)
return nil
})
}
// buildCurrentSceneClass0FixedUpperPacket builds the current scene-state upper
// envelope used by the same-client Python reference for class0 ITEM_LIST.
//
// This is an envelope builder, not a captured packet replay. The payload is
// still supplied by the caller from the real character inventory. Unlike the
// ordinary server upper envelope, this scene-state variant has a 16-byte
// header whose bytes 11..15 are all zero; its checksum covers those five zero
// bytes followed by the payload. The current client retains this protected
// reader state for lazy container UI construction, so substituting an ordinary
// incrementing upper sequence can parse the initial body yet fail later when
// the warehouse window consumes it.
func buildCurrentSceneClass0FixedUpperPacket(msgID uint16, body []byte) ([]byte, error) {
const headerSize = dnfproto.GameServerUpperHeaderSize16
packet := make([]byte, headerSize+len(body))
packet[0] = 0
binary.LittleEndian.PutUint16(packet[1:3], msgID)
binary.LittleEndian.PutUint32(packet[3:7], uint32(len(packet)))
copy(packet[headerSize:], body)
sum, err := dnfproto.ChecksumRange(packet, 11, len(packet)-11)
if err != nil {
return nil, err
}
binary.LittleEndian.PutUint32(packet[7:11], sum)
return packet, nil
}
// buildDeleteItemCompletionBody 构造空的 S0/0012 库存动作完成体:u16 计数=0。
// 当前 EXE 的 S0/0012 reader 即使 entry_count=0 也会收束共享库存动作等待态;
// 与 us90 build_delete_item_completion_0012() 的 struct.pack("<H", 0) 一致。
func buildDeleteItemCompletionBody() []byte {
return []byte{0x00, 0x00}
}
func (s *Service) sendGameUpperClass0Fixed16ZeroPrefix(session *gameSession, msgID uint16, body []byte, owner string) error {
packet, err := buildCurrentSceneClass0FixedUpperPacket(msgID, body)
if err != nil {
return err
}
runtimeName := upperRuntimeCmdPacketName(msgID, 0)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, 0)
s.logPacketEvent("game-upper-fixed16-zero-prefix-send",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"owner", owner,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "fixed16_zero_prefix_checksum",
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", false,
"body_codec", "plaintext",
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"header_seq", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "fixed16_zero_prefix_checksum",
"owner", owner,
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", false,
"body_codec", "plaintext")
return s.writeGameUpperPacket(session, packet)
}
// sendCurrentSceneItemList sends the real ITEM_LIST body through the scene
// fixed16 envelope. It deliberately does not consume session.upperSeq: that
// sequence belongs to ordinary upper packets, while this scene-state envelope
// owns an all-zero trailing header region.
func (s *Service) sendCurrentSceneItemList(session *gameSession, msgID uint16, body []byte) error {
// Main-inventory rows travel in descending slot order; see
// reverseCurrentMainItemListRows for the live evidence behind the reorder.
body = reverseCurrentMainItemListRows(body)
packet, err := buildCurrentSceneClass0FixedUpperPacket(msgID, body)
if err != nil {
return err
}
runtimeName := upperRuntimeCmdPacketName(msgID, 0)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, 0)
s.logPacketEvent("game-upper-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"sequence", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "current_scene_fixed16_zero_prefix",
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", false,
"body_codec", "plaintext",
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"header_seq", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "current_scene_fixed16_zero_prefix",
"plain_body_len", len(body),
"body_len", len(body),
"body_encoded", false,
"body_codec", "plaintext")
return s.writeGameUpperPacket(session, packet)
}
// sendCurrentProtectedClass0Packet owns current marker=1 scene transport
// frames whose payload is built from live state. It intentionally does not
// use the DOVE replay sequence: the fixed marker16 envelope is reconstructed
// here and the caller supplies the current structured body.
func (s *Service) sendCurrentProtectedClass0Packet(session *gameSession, msgID uint16, body []byte, bodyCodec, owner string) error {
const (
headerSize = dnfproto.GameServerUpperHeaderSize16
marker = uint32(1)
)
packet := make([]byte, headerSize+len(body))
packet[0] = 0
binary.LittleEndian.PutUint16(packet[1:3], msgID)
binary.LittleEndian.PutUint32(packet[3:7], uint32(len(packet)))
binary.LittleEndian.PutUint32(packet[7:11], marker)
packet[15] = byte(marker)
copy(packet[headerSize:], body)
runtimeName := upperRuntimeCmdPacketName(msgID, 0)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, 0)
s.logPacketEvent("game-upper-current-protected-scene-send",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"owner", owner,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"header_size", headerSize,
"header_marker", marker,
"body_len", len(body),
"body_encoded", true,
"body_codec", bodyCodec,
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", 0,
"header_seq", 0,
"header_size", headerSize,
"header_marker", marker,
"owner", owner,
"body_len", len(body),
"body_encoded", true,
"body_codec", bodyCodec)
return s.writeGameUpperPacket(session, packet)
}
// sendCurrentSceneFixedClass0Packet writes a real current scene notification
// through the fixed16 envelope. It is intentionally separate from ordinary
// upper packets: bytes 11..15 must remain zero for client-side scene state
// owners that lazily consume the notification after scene construction.
// reusableCurrentSceneFixedClass0Packet 是一个已经建好的 fixed-class0 上帧
// 及其日志元数据。
//
// 存在的理由:buildCurrentSceneClass0FixedUpperPacket 与两次
// upperRuntimeCmdPacket* 查询都只取决于 (msgID, body),而城镇移动广播
// (broadcastTownPlayerMove)会把同一份 body 发给同场景的每一个人。改前是逐个
// 收件人重复走一遍 sendCurrentSceneFixedClass0Packet,于是每个 op35 在 P 个人
// 的场景里白做 P-1 次分配与 2(P-1) 次查表——建包结果却完全一样。
//
// packet 在这之后只读:net.Conn.Write 不修改缓冲区,批量写入走
// append(session.upperWriteBatch, wire...) 复制,dproto 分支也会先复制。
type reusableCurrentSceneFixedClass0Packet struct {
msgID uint16
packet []byte
bodyLen int
runtimeName string
runtimeKnown bool
owner string
}
func buildReusableCurrentSceneFixedClass0Packet(
msgID uint16,
body []byte,
owner string,
) (reusableCurrentSceneFixedClass0Packet, error) {
packet, err := buildCurrentSceneClass0FixedUpperPacket(msgID, body)
if err != nil {
return reusableCurrentSceneFixedClass0Packet{}, err
}
return reusableCurrentSceneFixedClass0Packet{
msgID: msgID,
packet: packet,
bodyLen: len(body),
runtimeName: upperRuntimeCmdPacketName(msgID, 0),
runtimeKnown: upperRuntimeCmdPacketKnown(msgID, 0),
owner: owner,
}, nil
}
// sendReusableCurrentSceneFixedClass0Packet 把已经建好的包发给一个会话。
// 日志字段与逐收件人建包时逐字一致(含每会话自增的 pkt_seq)。
func (s *Service) sendReusableCurrentSceneFixedClass0Packet(
session *gameSession,
reusable reusableCurrentSceneFixedClass0Packet,
) error {
s.logPacketEvent("game-upper-current-scene-fixed-send",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"owner", reusable.owner,
"msg_id", reusable.msgID,
"runtime_cmd_name", reusable.runtimeName,
"runtime_cmd_known", reusable.runtimeKnown,
"classification", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "current_scene_fixed16_zero_prefix",
"body_len", reusable.bodyLen,
"packet_len", len(reusable.packet))
s.logGamePacket(session, "SEND", "game-upper", reusable.packet,
"msg_id", reusable.msgID,
"runtime_cmd_name", reusable.runtimeName,
"runtime_cmd_known", reusable.runtimeKnown,
"classification", 0,
"header_seq", 0,
"header_size", dnfproto.GameServerUpperHeaderSize16,
"header_mode", "current_scene_fixed16_zero_prefix",
"owner", reusable.owner,
"body_len", reusable.bodyLen)
return s.writeGameUpperPacket(session, reusable.packet)
}
func (s *Service) sendCurrentSceneFixedClass0Packet(session *gameSession, msgID uint16, body []byte, owner string) error {
reusable, err := buildReusableCurrentSceneFixedClass0Packet(msgID, body, owner)
if err != nil {
return err
}
return s.sendReusableCurrentSceneFixedClass0Packet(session, reusable)
}
func (s *Service) sendGameUpperRawClass(session *gameSession, msgID uint16, body []byte, classification byte) error {
if classification == 0 && msgID == uint16(dnfenum.CmdPacketWalkoutPartyMember) {
prepared, err := s.prepareCurrentPremiumContractItemUpdate(session, body)
if err != nil {
return err
}
body = prepared
}
return s.sendGameUpperRawClassCodec(session, msgID, body, classification, true)
}
func (s *Service) sendGameUpperRawClassNoCodec(session *gameSession, msgID uint16, body []byte, classification byte) error {
return s.sendGameUpperRawClassCodec(session, msgID, body, classification, false)
}
func (s *Service) sendGameUpperRawClassCodec(session *gameSession, msgID uint16, body []byte, classification byte, allowCodec bool) error {
return s.withGameWire(session, func() error {
sequence := session.upperSeq
headerSize := s.gameUpperHeaderSize()
wireBody := body
bodyEncoded := false
bodyCodec := s.gameUpperBodyCodecMode()
requireCurrentClass0Opcode1Codec := classification == 0 && msgID == 1
if allowCodec && requireCurrentClass0Opcode1Codec {
wireBody = dnfproto.EncodeLatestUpperClass0Opcode1Body(body)
bodyEncoded = true
bodyCodec = "current_class0_op1_xor_b5_rotl2"
} else if allowCodec && classification == dnfproto.DefaultChannelClassification && bodyCodec != gameUpperBodyCodecPlain {
encodedBody, encoded, err := dnfproto.EncodeLatestUpperServerBody(msgID, body)
if err != nil {
return err
}
wireBody = encodedBody
bodyEncoded = encoded
} else if !allowCodec {
bodyCodec = gameUpperBodyCodecPlain
}
// S2C 回包二次加密(见 protocol/s2ccipher.go):只加密 body,长度不变,
// 且**必须在算 checksum 之前**完成——checksum 覆盖 seq+body,客户端按线上字节校验。
if s.applyGameS2CBodyCipher(session, wireBody, msgID, sequence) {
bodyEncoded = true
bodyCodec = dnfproto.S2CCipherCodecName
}
packet, err := dnfproto.BuildGameServerUpperPacketWithHeaderSize(msgID, wireBody, sequence, classification, headerSize)
if err != nil {
return err
}
session.upperSeq++
runtimeName := upperRuntimeCmdPacketName(msgID, classification)
runtimeKnown := upperRuntimeCmdPacketKnown(msgID, classification)
s.logPacketEvent("game-upper-send-meta",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"sequence", sequence,
"header_size", headerSize,
"plain_body_len", len(body),
"body_len", len(wireBody),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec,
"packet_len", len(packet))
s.logGamePacket(session, "SEND", "game-upper", packet,
"msg_id", msgID,
"runtime_cmd_name", runtimeName,
"runtime_cmd_known", runtimeKnown,
"classification", classification,
"header_seq", sequence,
"header_size", headerSize,
"plain_body_len", len(body),
"body_len", len(wireBody),
"body_encoded", bodyEncoded,
"body_codec", bodyCodec)
return s.writeGameUpperPacketLocked(session, packet)
})
}
func (s *Service) gameUpperHeaderSize() int {
if s.options.gameUpperHeader == gameUpperHeaderServer16 {
return dnfproto.GameServerUpperHeaderSize16
}
return dnfproto.GameServerUpperHeaderSize
}
func (s *Service) gameUpperBodyCodecMode() string {
if s.options.gameUpperBodyCodec == "" {
return gameUpperBodyCodecAuto
}
return s.options.gameUpperBodyCodec
}
func (s *Service) gameUpperClientBodyCodecMode() string {
if s.options.gameUpperClientBodyCodec == "" {
return gameUpperClientBodyCodecPlain
}
return s.options.gameUpperClientBodyCodec
}
func (s *Service) sendGameUpperSuccess(session *gameSession, msgID uint16, body []byte) error {
return s.sendGameUpperRaw(session, msgID, upperSuccessBody(body))
}
func (s *Service) sendGameUpperFailure(session *gameSession, msgID uint16, code byte) error {
body := []byte{0, code}
if currentAuctionFailureNeedsPayFeature(msgID, code) {
body = append(body, currentAuctionPayTypeGold)
}
return s.sendGameUpperRaw(session, msgID, body)
}
func upperSuccessBody(body []byte) []byte {
out := make([]byte, 1+len(body))
out[0] = 1
copy(out[1:], body)
return out
}
func (s *Service) logGamePacket(session *gameSession, direction string, kind string, data []byte, fields ...any) {
if session == nil {
s.logPacket(direction, kind, data, fields...)
return
}
packetSeq := atomic.AddUint64(&session.packetSeq, 1)
args := make([]any, 0, len(fields)+4)
args = append(args, "conn_id", session.connID, "pkt_seq", packetSeq)
args = append(args, fields...)
s.logPacket(direction, kind, data, args...)
}
func (s *Service) logGameEvent(session *gameSession, kind string, fields ...any) {
if session == nil {
s.logPacketEvent(kind, fields...)
return
}
args := make([]any, 0, len(fields)+4)
args = append(args, "conn_id", session.connID, "channel_id", session.channel.ID)
args = append(args, fields...)
s.logPacketEvent(kind, args...)
}