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("