DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,160 @@
|
||||
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"
|
||||
}
|
||||
Reference in new issue
Block a user