Files
90CNServer/server/internal/services/dnfbridge/dungeon_tournament_protocol.go
T
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

261 lines
8.8 KiB
Go

package dnfbridge
import (
"errors"
"fmt"
"longheng.io/server/internal/modules/dnf/worldmap"
)
const (
currentTournamentInfoNotification uint16 = 386
currentTournamentMapInfoNotification uint16 = 387
currentTournamentClearRewardNotification uint16 = 388
currentTournamentRewardSelectState uint16 = 457
currentTournamentRewardSelect uint16 = 458
currentTournamentEmptyRewardSentinel uint32 = ^uint32(0)
currentTournamentUnselectedPartySlot byte = 0xff
currentTournamentSoloPartySlot byte = 0
)
var currentTournamentRewardSelectOpenBody = []byte{
1,
1, currentTournamentUnselectedPartySlot, currentTournamentUnselectedPartySlot, currentTournamentUnselectedPartySlot,
1, currentTournamentUnselectedPartySlot, currentTournamentUnselectedPartySlot, currentTournamentUnselectedPartySlot,
}
var (
errTournamentProtocolRuntime = errors.New("dnf tournament protocol runtime is required")
errTournamentProtocolRoundCount = errors.New("dnf tournament protocol requires four rounds")
errTournamentProtocolTeamCount = errors.New("dnf tournament protocol team count exceeds current EXE range")
errTournamentProtocolActorCount = errors.New("dnf tournament protocol actor count exceeds current EXE range")
errTournamentProtocolMapOperation = errors.New("dnf tournament map-info operation is invalid")
errTournamentProtocolRewardCount = errors.New("dnf tournament reward count exceeds current EXE range")
errTournamentProtocolRewardRequest = errors.New("dnf tournament reward request is invalid")
)
func buildCurrentTournamentInfoBody(runtime *worldmap.TournamentRuntime, difficulty byte) ([]byte, error) {
if runtime == nil {
return nil, errTournamentProtocolRuntime
}
if len(runtime.Rounds) != 4 {
return nil, fmt.Errorf("%w: count=%d", errTournamentProtocolRoundCount, len(runtime.Rounds))
}
partyCount := int(runtime.Definition.PartyCount)
if partyCount < 1 || partyCount > 0xff || len(runtime.PathActors) != len(runtime.Rounds)*partyCount {
return nil, fmt.Errorf("%w: party=%d path_actors=%d", errTournamentProtocolActorCount, partyCount, len(runtime.PathActors))
}
firstSequence := runtime.FirstActorSequence()
if firstSequence == 0 {
return nil, worldmap.ErrTournamentSequenceUnbound
}
if int(firstSequence)+len(runtime.PathActors)-1 > int(^uint16(0)) {
return nil, fmt.Errorf("%w: first=%d count=%d", errTournamentProtocolActorCount, firstSequence, len(runtime.PathActors))
}
var writer packetWriter
writer.writeUint32(runtime.Definition.DungeonID)
writer.writeByte(difficulty)
writer.writeByte(byte(partyCount))
for _, round := range runtime.Rounds {
if len(round.Teams) > 0xff {
return nil, fmt.Errorf("%w: round=%d count=%d", errTournamentProtocolTeamCount, round.Number, len(round.Teams))
}
writer.writeByte(round.Number)
writer.writeByte(byte(len(round.Teams)))
for _, team := range round.Teams {
if len(team.Members) != partyCount {
return nil, fmt.Errorf("%w: round=%d team=%d members=%d party=%d", errTournamentProtocolActorCount, round.Number, team.Position, len(team.Members), partyCount)
}
writer.writeByte(team.Position)
for _, actor := range team.Members {
writer.writeUint32(actor.Code)
writer.writeUint16(actor.Strength)
}
}
}
actorIndex := 0
for _, round := range runtime.Rounds {
writer.writeByte(round.Number)
for member := 0; member < partyCount; member++ {
actor := runtime.PathActors[actorIndex]
writer.writeUint16(firstSequence + uint16(actorIndex))
writer.writeUint32(actor.Code)
writer.writeByte(actor.Level)
writer.writeByte(actor.Type)
actorIndex++
}
}
return writer.bytes(), nil
}
type currentTournamentMapActor struct {
Sequence uint16
Code uint32
FieldA uint16
FieldB uint16
FieldC byte
FieldD byte
FieldE byte
FieldF byte
}
type currentTournamentMapExtra struct {
Type byte
ValueA uint32
ValueB uint32
ValueC uint32
ValueD uint16
ValueE byte
ValueF uint16
ValueG uint16
}
type currentTournamentMapInfo struct {
RoomX byte
RoomY byte
Seed uint32
RoomState byte
Operation byte
MapID uint32
Actors []currentTournamentMapActor
Extras []currentTournamentMapExtra
FinalFlag byte
}
func (packet currentTournamentMapInfo) Build() ([]byte, error) {
if packet.Operation > 2 {
return nil, fmt.Errorf("%w: operation=%d", errTournamentProtocolMapOperation, packet.Operation)
}
if packet.Operation == 0 && (packet.MapID != 0 || len(packet.Actors) != 0 || len(packet.Extras) != 0 || packet.FinalFlag != 0) {
return nil, fmt.Errorf("%w: operation=0 has payload", errTournamentProtocolMapOperation)
}
if packet.Operation != 0 && packet.MapID == 0 {
return nil, fmt.Errorf("%w: operation=%d map=0", errTournamentProtocolMapOperation, packet.Operation)
}
if len(packet.Actors) > 0xff || len(packet.Extras) > 0xff {
return nil, fmt.Errorf("%w: actors=%d extras=%d", errTournamentProtocolActorCount, len(packet.Actors), len(packet.Extras))
}
var writer packetWriter
writer.writeByte(packet.RoomX)
writer.writeByte(packet.RoomY)
writer.writeUint32(packet.Seed)
writer.writeByte(packet.RoomState)
writer.writeByte(packet.Operation)
if packet.Operation == 0 {
return writer.bytes(), nil
}
writer.writeUint32(packet.MapID)
writer.writeByte(byte(len(packet.Actors)))
for _, actor := range packet.Actors {
writer.writeUint16(actor.Sequence)
writer.writeUint32(actor.Code)
writer.writeUint16(actor.FieldA)
writer.writeUint16(actor.FieldB)
writer.writeByte(actor.FieldC)
writer.writeByte(actor.FieldD)
writer.writeByte(actor.FieldE)
writer.writeByte(actor.FieldF)
}
writer.writeByte(byte(len(packet.Extras)))
for _, extra := range packet.Extras {
writer.writeByte(extra.Type)
writer.writeUint32(extra.ValueA)
writer.writeUint32(extra.ValueB)
writer.writeUint32(extra.ValueC)
writer.writeUint16(extra.ValueD)
writer.writeByte(extra.ValueE)
writer.writeUint16(extra.ValueF)
writer.writeUint16(extra.ValueG)
}
writer.writeByte(packet.FinalFlag)
return writer.bytes(), nil
}
type currentTournamentClearRewardExtended struct {
Prefix uint16
ValueA uint32
ValueB uint32
ValueC uint16
SuffixA byte
SuffixB byte
}
type currentTournamentClearRewardEntry struct {
ItemOrSentinel uint32
CountOrGold uint32
Reserved uint16
}
type currentTournamentClearReward struct {
CompletedRounds byte
Experience uint32
Extended []currentTournamentClearRewardExtended
GroupA []currentTournamentClearRewardEntry
GroupB []currentTournamentClearRewardEntry
}
func (packet currentTournamentClearReward) Build() ([]byte, error) {
if len(packet.Extended) > 0xff || len(packet.GroupA) > 0xff || len(packet.GroupB) > 0xff {
return nil, fmt.Errorf("%w: extended=%d group_a=%d group_b=%d", errTournamentProtocolRewardCount, len(packet.Extended), len(packet.GroupA), len(packet.GroupB))
}
var writer packetWriter
writer.writeByte(packet.CompletedRounds)
writer.writeUint32(packet.Experience)
writer.writeByte(byte(len(packet.Extended)))
for _, row := range packet.Extended {
writer.writeUint16(row.Prefix)
writer.writeUint32(row.ValueA)
writer.writeUint32(row.ValueB)
writer.writeUint16(row.ValueC)
writer.writeByte(row.SuffixA)
writer.writeByte(row.SuffixB)
}
for _, group := range [][]currentTournamentClearRewardEntry{packet.GroupA, packet.GroupB} {
writer.writeByte(byte(len(group)))
for _, row := range group {
writer.writeUint32(row.ItemOrSentinel)
writer.writeUint32(row.CountOrGold)
writer.writeUint16(row.Reserved)
}
}
return writer.bytes(), nil
}
type currentTournamentRewardSelectRequest struct {
Side byte
CardIndex byte
}
func decodeCurrentTournamentRewardSelectRequest(body []byte) (currentTournamentRewardSelectRequest, error) {
if len(body) != 2 {
return currentTournamentRewardSelectRequest{}, fmt.Errorf("%w: body_len=%d want=2", errTournamentProtocolRewardRequest, len(body))
}
return currentTournamentRewardSelectRequest{Side: body[0], CardIndex: body[1]}, nil
}
// currentTournamentRewardSelectionState is the class-1 op458 response. The
// current EXE consumes the leading success byte before its handler, then reads
// one count and one selected-party byte per card for each of the two groups.
// Counts must therefore be derived from the same frozen plan used by op388.
type currentTournamentRewardSelectionState struct {
Plan worldmap.TournamentRewardPlan
}
func (state currentTournamentRewardSelectionState) build() ([]byte, error) {
var writer packetWriter
writer.writeByte(1)
for side := byte(0); side < 2; side++ {
count := state.Plan.CardCount(side)
writer.writeByte(count)
selected, selectedOK := state.Plan.SelectedCard(side)
for index := byte(0); index < count; index++ {
partySlot := currentTournamentUnselectedPartySlot
if selectedOK && selected == index {
partySlot = currentTournamentSoloPartySlot
}
writer.writeByte(partySlot)
}
}
return writer.bytes(), nil
}