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,192 @@
|
||||
// This file owns the current-EXE creature-list body. Inventory list-7 rows
|
||||
// are delegated to inventory.BuildPetItemListRefreshBody so both paths use
|
||||
// the same proved 0x77-byte item row.
|
||||
package pet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dnfinventory "longheng.io/server/internal/modules/dnf/inventory"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
errCreatureCountTooLarge = errors.New("creature count exceeds 255")
|
||||
errCreatureKeyInvalid = errors.New("creature key is invalid")
|
||||
errCreatureModeInvalid = errors.New("creature mode flag is invalid")
|
||||
errCreatureGrowthInvalid = errors.New("creature growth state is invalid")
|
||||
errCreatureNameTooLong = errors.New("creature name exceeds 30 bytes")
|
||||
errCreatureTailInvalid = errors.New("creature tail flag is invalid")
|
||||
)
|
||||
|
||||
// BuildCreatureListBody exposes the current NoPack class0/op105 business body
|
||||
// to the bridge's scene-lifecycle owner. Callers must still choose a proven
|
||||
// scene-ready send point; this function only owns the exact typed body.
|
||||
func BuildCreatureListBody(entries []dnfrepo.PetEntry) ([]byte, error) {
|
||||
return buildCreatureListBody(entries)
|
||||
}
|
||||
|
||||
// BuildCreatureGrowthBody follows current NoPack sub_1D5AF60 exactly. This is
|
||||
// the scene class0/op102 direction and must not be confused with the unrelated
|
||||
// C2S hatch request or its class1/status ACK, which share the numeric opcode.
|
||||
func BuildCreatureGrowthBody(entry dnfrepo.PetEntry) ([]byte, error) {
|
||||
if entry.ModeFlag > 1 {
|
||||
return nil, fmt.Errorf("%w: mode=%d", errCreatureModeInvalid, entry.ModeFlag)
|
||||
}
|
||||
if entry.Level < 1 || entry.Level > 255 || entry.Exp < 0 || entry.Exp > int64(^uint32(0)) {
|
||||
return nil, fmt.Errorf("%w: level=%d progress=%d", errCreatureGrowthInvalid, entry.Level, entry.Exp)
|
||||
}
|
||||
body := make([]byte, 6, 8)
|
||||
body[0] = byte(entry.Level)
|
||||
body[1] = entry.ModeFlag
|
||||
binary.LittleEndian.PutUint32(body[2:6], uint32(entry.Exp))
|
||||
if entry.ModeFlag == 1 {
|
||||
body = append(body, entry.Mode1Field0A, entry.Mode1Field0B)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type packetWriter struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *packetWriter) writeByte(value byte) {
|
||||
w.buf = append(w.buf, value)
|
||||
}
|
||||
|
||||
func (w *packetWriter) writeUint32(value uint32) {
|
||||
var tmp [4]byte
|
||||
binary.LittleEndian.PutUint32(tmp[:], value)
|
||||
w.buf = append(w.buf, tmp[:]...)
|
||||
}
|
||||
|
||||
func (w *packetWriter) writeRawDstr(value []byte) {
|
||||
w.writeUint32(uint32(len(value)))
|
||||
w.buf = append(w.buf, value...)
|
||||
}
|
||||
|
||||
func (w *packetWriter) bytes() []byte {
|
||||
return append([]byte(nil), w.buf...)
|
||||
}
|
||||
|
||||
func buildSuccessAck() []byte {
|
||||
return []byte{1}
|
||||
}
|
||||
|
||||
// buildRenameCreatureSuccessAck follows current NoPack sub_1D1D5C0 exactly:
|
||||
// u16 slot + u8 list_type. The response does not echo the name.
|
||||
func buildRenameCreatureSuccessAck(slotIndex int16, listType byte) []byte {
|
||||
body := make([]byte, 3)
|
||||
binary.LittleEndian.PutUint16(body[0:2], uint16(slotIndex))
|
||||
body[2] = listType
|
||||
return body
|
||||
}
|
||||
|
||||
// buildRenameCreatureNameNotificationBody follows current NoPack
|
||||
// sub_1D84FE0 exactly: u16 actor object key followed by the renamed creature
|
||||
// DSTR. This is the native in-place rename notification; it updates the
|
||||
// already-created actor/creature objects without replaying mode0 or replacing
|
||||
// the complete op105 creature table.
|
||||
func buildRenameCreatureNameNotificationBody(objectKey uint16, nameRaw []byte) []byte {
|
||||
body := make([]byte, 2+currentDstrLengthBytes+len(nameRaw))
|
||||
binary.LittleEndian.PutUint16(body[0:2], objectKey)
|
||||
binary.LittleEndian.PutUint32(body[2:6], uint32(len(nameRaw)))
|
||||
copy(body[6:], nameRaw)
|
||||
return body
|
||||
}
|
||||
|
||||
// buildCreatureListBody follows current NoPack sub_1D57AB0 exactly:
|
||||
// u8 count; repeat {u32 key,u8 satiety,u8 mode,u32 exp,
|
||||
// [mode==1:u8,u8],u8 level,DSTR rawName,u8 tailFlag}.
|
||||
func buildCreatureListBody(entries []dnfrepo.PetEntry) ([]byte, error) {
|
||||
if len(entries) > 255 {
|
||||
return nil, errCreatureCountTooLarge
|
||||
}
|
||||
ordered := append([]dnfrepo.PetEntry(nil), entries...)
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
left, leftOK := creatureWireKey(ordered[i])
|
||||
right, rightOK := creatureWireKey(ordered[j])
|
||||
if leftOK != rightOK {
|
||||
return leftOK
|
||||
}
|
||||
if left != right {
|
||||
return left < right
|
||||
}
|
||||
return ordered[i].PetKey < ordered[j].PetKey
|
||||
})
|
||||
|
||||
var writer packetWriter
|
||||
writer.writeByte(byte(len(ordered)))
|
||||
for _, entry := range ordered {
|
||||
key, ok := creatureWireKey(entry)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q", errCreatureKeyInvalid, entry.PetKey)
|
||||
}
|
||||
if entry.ModeFlag > 1 {
|
||||
return nil, fmt.Errorf("%w: %d", errCreatureModeInvalid, entry.ModeFlag)
|
||||
}
|
||||
// sub_1D57AB0 passes max=30 to upper_pkt_read_wstr. Its reader accepts
|
||||
// only lengths strictly below max; a 30-byte name would be rejected
|
||||
// without consuming the bytes and desynchronize the remainder.
|
||||
if len(entry.NameRaw) >= 30 {
|
||||
return nil, fmt.Errorf("%w: %d", errCreatureNameTooLong, len(entry.NameRaw))
|
||||
}
|
||||
if entry.TailFlag > 1 {
|
||||
return nil, fmt.Errorf("%w: %d", errCreatureTailInvalid, entry.TailFlag)
|
||||
}
|
||||
|
||||
writer.writeUint32(key)
|
||||
writer.writeByte(entry.Satiety)
|
||||
writer.writeByte(entry.ModeFlag)
|
||||
writer.writeUint32(clampUint32(entry.Exp))
|
||||
if entry.ModeFlag == 1 {
|
||||
writer.writeByte(entry.Mode1Field0A)
|
||||
writer.writeByte(entry.Mode1Field0B)
|
||||
}
|
||||
writer.writeByte(clampByte(entry.Level))
|
||||
writer.writeRawDstr(entry.NameRaw)
|
||||
writer.writeByte(entry.TailFlag)
|
||||
}
|
||||
return writer.bytes(), nil
|
||||
}
|
||||
|
||||
func creatureWireKey(entry dnfrepo.PetEntry) (uint32, bool) {
|
||||
if entry.CreatureKey != 0 {
|
||||
return entry.CreatureKey, true
|
||||
}
|
||||
raw := strings.TrimSpace(entry.PetKey)
|
||||
value, err := strconv.ParseUint(raw, 10, 32)
|
||||
if err != nil || value == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return uint32(value), true
|
||||
}
|
||||
|
||||
func buildPetItemListRefreshBody(slots map[string]dnfrepo.ItemStack) []byte {
|
||||
return dnfinventory.BuildPetItemListRefreshBody(slots)
|
||||
}
|
||||
|
||||
func clampUint32(value int64) uint32 {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if value > int64(^uint32(0)) {
|
||||
return ^uint32(0)
|
||||
}
|
||||
return uint32(value)
|
||||
}
|
||||
|
||||
func clampByte(value int64) byte {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 255 {
|
||||
return 255
|
||||
}
|
||||
return byte(value)
|
||||
}
|
||||
Reference in new issue
Block a user