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

161 lines
5.8 KiB
Go

package dnfbridge
import (
"encoding/binary"
"errors"
"fmt"
"longheng.io/server/internal/modules/dnf/worldmap"
)
var (
errDungeonDeathResponseKind = errors.New("dnf dungeon death response kind is unsupported")
errDungeonDeathResponseKeyRange = errors.New("dnf dungeon death response object key exceeds current EXE range")
errDungeonDeathDropCountRange = errors.New("dnf dungeon death response drop count exceeds current EXE range")
errDungeonDeathDropItemInvalid = errors.New("dnf dungeon death response drop item is invalid")
errDungeonDeathRoomCoordinateRange = errors.New("dnf dungeon death response room coordinate exceeds current EXE range")
)
type currentDungeonDeathResponseKind string
const (
currentDungeonDeathResponseMonster currentDungeonDeathResponseKind = "monster"
currentDungeonDeathResponseAICharacter currentDungeonDeathResponseKind = "ai_character"
)
const currentDungeonZeroDropDeathBodySize = 7
const currentDungeonDeathDropWireSize = 4 + currentItemListEntryWireSize + 2 + 2
// currentDungeonDeathDropWire mirrors only the fields proven by the current
// NoPack sub_1D4E680 reader. SceneObjectKey is the same u32 later echoed by the
// ordinary op43 request and class0/op39 removal response. The first trailing
// u16 remains a constructor sentinel with otherwise unproved semantics.
type currentDungeonDeathDropWire struct {
SceneObjectKey uint32
Item currentItemListEntry
UnknownTailSentinel uint16
OwnerActorObjectKey uint16
}
func buildCurrentDungeonDeathNotificationBody(
objectKey uint32,
kind currentDungeonDeathResponseKind,
) ([]byte, error) {
return buildCurrentDungeonDeathNotificationBodyWithDrops(objectKey, kind, nil)
}
func buildCurrentDungeonDeathNotificationBodyWithDrops(
objectKey uint32,
kind currentDungeonDeathResponseKind,
drops []currentDungeonDeathDropWire,
optionalRoom ...worldmap.RoomCoordinate,
) ([]byte, error) {
switch kind {
case currentDungeonDeathResponseMonster, currentDungeonDeathResponseAICharacter:
default:
return nil, errDungeonDeathResponseKind
}
if objectKey == 0 || objectKey > uint32(^uint16(0)) {
return nil, fmt.Errorf("%w: object_key=%d", errDungeonDeathResponseKeyRange, objectKey)
}
if len(drops) > int(^uint8(0)) {
return nil, fmt.Errorf("%w: count=%d", errDungeonDeathDropCountRange, len(drops))
}
if len(optionalRoom) > 1 {
return nil, fmt.Errorf("%w: optional_room_count=%d", errDungeonDeathRoomCoordinateRange, len(optionalRoom))
}
if len(optionalRoom) == 1 && (optionalRoom[0].X < 0 || optionalRoom[0].X > 0xff || optionalRoom[0].Y < 0 || optionalRoom[0].Y > 0xff) {
return nil, fmt.Errorf("%w: room=%s", errDungeonDeathRoomCoordinateRange, optionalRoom[0])
}
for index, drop := range drops {
itemID := binary.LittleEndian.Uint32(drop.Item.data[0x02:0x06])
amount := binary.LittleEndian.Uint32(drop.Item.data[0x06:0x0A])
if itemID == 0 && amount == 0 {
return nil, fmt.Errorf("%w: index=%d item_id=0 amount=0", errDungeonDeathDropItemInvalid, index)
}
}
// Current NoPack class-0/op38 reads:
// u16 target key, u8 drop count,
// count * (u32 + raw[0x77] + u16 + owner u16),
// then u8 post flag, u8 mode, u8 progression, u8 optional-room flag.
// The zero-drop form remains the canonical seven-byte body.
var writer packetWriter
writer.writeUint16(uint16(objectKey))
writer.writeByte(byte(len(drops)))
for _, drop := range drops {
writer.writeUint32(drop.SceneObjectKey)
writer.writeBytes(drop.Item.data[:])
writer.writeUint16(drop.UnknownTailSentinel)
writer.writeUint16(drop.OwnerActorObjectKey)
}
writer.writeByte(0) // post flag: no optional u16
writer.writeByte(0) // mode 0
if len(optionalRoom) == 1 {
writer.writeByte(0xff) // no progression update
writer.writeByte(1)
writer.writeByte(byte(optionalRoom[0].X))
writer.writeByte(byte(optionalRoom[0].Y))
} else {
writer.writeByte(0xff) // no progression update
writer.writeByte(0)
}
return writer.bytes(), nil
}
func currentDungeonDeathDropItemState(
sceneSlot uint16,
definition dungeonDropItemDefinition,
amount uint32,
qualitySeed ...uint32,
) (currentItemListEntry, error) {
if sceneSlot == 0 || amount == 0 {
return currentItemListEntry{}, fmt.Errorf(
"%w: scene_slot=%d item_id=%d amount=%d",
errDungeonDeathDropItemInvalid,
sceneSlot,
definition.ItemID,
amount,
)
}
instanceValue := amount
if definition.Kind == dungeonDropItemEquipment {
instanceValue = 0
if len(qualitySeed) > 0 && validCurrentEquipmentQualitySeed(qualitySeed[0]) {
instanceValue = qualitySeed[0]
}
if instanceValue == 0 {
var err error
instanceValue, err = newCurrentEquipmentQualitySeed()
if err != nil {
return currentItemListEntry{}, err
}
}
}
var entry currentItemListEntry
entry.patchCore(int16(sceneSlot), definition.ItemID, instanceValue)
if definition.Kind == dungeonDropItemEquipment && definition.Durability != 0 {
entry.setUint16(0x0B, definition.Durability)
}
if currentDungeonDroppedEquipmentShouldBeSealed(definition) {
entry.setByte(0x0D, 1)
}
return entry, nil
}
// currentDungeonDroppedEquipmentShouldBeSealed decides whether a freshly
// created equipment instance starts sealed (row byte +0x0D = 1, drawn by the
// client as 封装 / tradeable once). The runtime PVF owns that: only
// `[attach type] [sealing]` equipment is created sealed, exactly as the
// booster, magic-box, regeneration and mailbox grant paths already decide.
// The former rarity rule (rare / unique / legendary) sealed untradeable
// legendaries from quests, guild exchange and NPC gift boxes, so they showed
// 贴膜 on arrival; the correct state for those is unsealed and untradeable.
func currentDungeonDroppedEquipmentShouldBeSealed(definition dungeonDropItemDefinition) bool {
if definition.Kind != dungeonDropItemEquipment {
return false
}
return normalizeMagicBoxPVFType(definition.AttachType) == "sealing"
}