// 本文件解析任务类 C2S 请求体。 // C# QuestManager 会在 body 长度大于 2 时去掉前 2 字节 wire-type echo,再交给 QuestService。 package quest import ( "encoding/binary" "fmt" "longheng.io/server/internal/modules/dnf/dnfenum" ) const ( CurrentFinishQuestRequestBodySize = 10 CurrentFinishQuestObservedTailMarker = ^uint16(0) // CurrentSetTriggerRequestBodySize is the scalar op33 shape: // echo | quest | add/remove | reserved-or-answer. CurrentSetTriggerRequestBodySize = 6 // CurrentMaskedSetTriggerRequestBodySize is the current-client packed // objective shape used by multi-lane tasks such as the Hell Party // challenges: echo | quest | lane mask | add/remove | reserved. CurrentMaskedSetTriggerRequestBodySize = 7 ) // QuestIDRequest 描述只携带 questId 的任务请求。 type QuestIDRequest struct { QuestID uint16 } // SetTriggerRequest 描述任务触发进度请求。 type SetTriggerRequest struct { QuestID uint16 TriggerType byte TriggerArgument byte IsIncrement bool } // FinishQuestRequest 描述完成任务请求。 type FinishQuestRequest struct { QuestID uint16 RewardSelectIndex uint16 HasRewardSelect bool Multiplier uint16 Reserved uint16 } // DecodeQuestIDRequest 解析接取/放弃任务请求。 func DecodeQuestIDRequest(body []byte) (QuestIDRequest, error) { qBody := stripEcho(body) if len(qBody) < 2 { return QuestIDRequest{}, fmt.Errorf("body too short after echo strip: got %d want >= 2", len(qBody)) } return QuestIDRequest{QuestID: binary.LittleEndian.Uint16(qBody)}, nil } // DecodeSetTriggerRequest parses both proven current-client op33 forms. The // seven-byte form is not a transport trailer: its 0x10/0x20/0x40 mask selects // one 9-bit lane of the packed quest trigger. Older six-byte masked clones are // retained only as a compatibility input for an already-supported profile. func DecodeSetTriggerRequest(body []byte) (SetTriggerRequest, error) { if len(body) != CurrentSetTriggerRequestBodySize && len(body) != CurrentMaskedSetTriggerRequestBodySize { return SetTriggerRequest{}, fmt.Errorf("set quest trigger body size: got %d want %d or %d", len(body), CurrentSetTriggerRequestBodySize, CurrentMaskedSetTriggerRequestBodySize) } if echo := binary.LittleEndian.Uint16(body[:2]); echo != uint16(dnfenum.CmdPacketSetQuestTrigger) { return SetTriggerRequest{}, fmt.Errorf("set quest trigger echo: got %d want %d", echo, dnfenum.CmdPacketSetQuestTrigger) } request := SetTriggerRequest{QuestID: binary.LittleEndian.Uint16(body[2:4])} if request.QuestID == 0 { return SetTriggerRequest{}, fmt.Errorf("set quest trigger has zero quest ID") } if len(body) == CurrentMaskedSetTriggerRequestBodySize { switch body[4] { case 0x10, 0x20, 0x40: request.TriggerType = body[4] default: return SetTriggerRequest{}, fmt.Errorf("set quest trigger mask: got %#x want 0x10, 0x20, or 0x40", body[4]) } if body[5] > 1 { return SetTriggerRequest{}, fmt.Errorf("set quest trigger add/remove: got %d want boolean", body[5]) } if body[6] != 0 { return SetTriggerRequest{}, fmt.Errorf("set quest trigger masked reserved: got %#x want 0", body[6]) } request.IsIncrement = body[5] != 0 return request, nil } // Retain the established local six-byte clone unchanged. Its last two // bytes are trigger type and trigger argument; several existing quest // families (notably seeking) still use type 1 through that compatibility // route. The current masked Hell Party form above is unambiguous because it // is seven bytes. request.TriggerType = body[4] request.TriggerArgument = body[5] request.IsIncrement = body[5] != 0 return request, nil } // DecodeFinishQuestRequest 解析完成任务请求。 func DecodeFinishQuestRequest(body []byte) (FinishQuestRequest, error) { if len(body) != CurrentFinishQuestRequestBodySize { return FinishQuestRequest{}, fmt.Errorf("current EXE finish-quest body size: got %d want %d", len(body), CurrentFinishQuestRequestBodySize) } if echo := binary.LittleEndian.Uint16(body[0:2]); echo != uint16(dnfenum.CmdPacketFinishQuest) { return FinishQuestRequest{}, fmt.Errorf("current EXE finish-quest echo: got %d want %d", echo, dnfenum.CmdPacketFinishQuest) } req := FinishQuestRequest{ QuestID: binary.LittleEndian.Uint16(body[2:4]), RewardSelectIndex: binary.LittleEndian.Uint16(body[4:6]), Multiplier: binary.LittleEndian.Uint16(body[6:8]), Reserved: binary.LittleEndian.Uint16(body[8:10]), } req.HasRewardSelect = req.RewardSelectIndex != ^uint16(0) return req, nil } func stripEcho(body []byte) []byte { if body == nil || len(body) <= 2 { return body } return body[2:] } func (r QuestIDRequest) String() string { return fmt.Sprintf("quest=%d", r.QuestID) } func (r SetTriggerRequest) String() string { return fmt.Sprintf("quest=%d triggerType=%d argument=%d increment=%t", r.QuestID, r.TriggerType, r.TriggerArgument, r.IsIncrement) } func (r FinishQuestRequest) String() string { return fmt.Sprintf("quest=%d rewardSelect=%d hasReward=%t multiplier=%d reserved=%d", r.QuestID, r.RewardSelectIndex, r.HasRewardSelect, r.Multiplier, r.Reserved) }