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,206 @@
|
||||
// Package darkknightcombo owns Dark Knight combo pages independently from
|
||||
// ordinary learned skills and quick-slot layouts.
|
||||
package darkknightcombo
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
Job byte = 9
|
||||
MaximumPageCount = 2
|
||||
MaximumStageCount = 5
|
||||
maximumEntryCount = 255
|
||||
)
|
||||
|
||||
var ErrPageBlockInvalid = errors.New("dark knight combo page block is invalid")
|
||||
|
||||
type Root struct {
|
||||
SkillID uint16
|
||||
Children []uint16
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Index byte
|
||||
Roots []Root
|
||||
}
|
||||
|
||||
// PreloadEntry is one character's combo pages for the native character-select
|
||||
// preloader. Slot is the client roster slot, not a database row ordinal.
|
||||
type PreloadEntry struct {
|
||||
Slot byte
|
||||
Pages []Page
|
||||
}
|
||||
|
||||
// ParsePageBlock decodes the native editor page payload:
|
||||
// u8 page, u8 root_count, repeated(u16 root, u8 child_count, u16 children...).
|
||||
func ParsePageBlock(body []byte) (Page, error) {
|
||||
if len(body) < 2 {
|
||||
return Page{}, fmt.Errorf("%w: length=%d", ErrPageBlockInvalid, len(body))
|
||||
}
|
||||
page := Page{Index: body[0]}
|
||||
if page.Index >= MaximumPageCount {
|
||||
return Page{}, fmt.Errorf("%w: page=%d", ErrPageBlockInvalid, page.Index)
|
||||
}
|
||||
offset := 2
|
||||
rootCount := int(body[1])
|
||||
page.Roots = make([]Root, 0, rootCount)
|
||||
for rootIndex := 0; rootIndex < rootCount; rootIndex++ {
|
||||
if offset+3 > len(body) {
|
||||
return Page{}, fmt.Errorf("%w: truncated root=%d offset=%d", ErrPageBlockInvalid, rootIndex, offset)
|
||||
}
|
||||
root := Root{SkillID: binary.LittleEndian.Uint16(body[offset : offset+2])}
|
||||
offset += 2
|
||||
childCount := int(body[offset])
|
||||
offset++
|
||||
if offset+childCount*2 > len(body) {
|
||||
return Page{}, fmt.Errorf("%w: truncated children root=%d count=%d", ErrPageBlockInvalid, rootIndex, childCount)
|
||||
}
|
||||
root.Children = make([]uint16, 0, childCount)
|
||||
for childIndex := 0; childIndex < childCount; childIndex++ {
|
||||
root.Children = append(root.Children, binary.LittleEndian.Uint16(body[offset:offset+2]))
|
||||
offset += 2
|
||||
}
|
||||
page.Roots = append(page.Roots, root)
|
||||
}
|
||||
if offset != len(body) {
|
||||
return Page{}, fmt.Errorf("%w: trailing=%d", ErrPageBlockInvalid, len(body)-offset)
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// NormalizePage keeps the last occurrence of a child skill, matching the
|
||||
// native editor when a skill is moved within or between combo columns.
|
||||
func NormalizePage(page Page) (Page, error) {
|
||||
if page.Index >= MaximumPageCount || len(page.Roots) > maximumEntryCount {
|
||||
return Page{}, ErrPageBlockInvalid
|
||||
}
|
||||
type occurrence struct{ root, child int }
|
||||
last := make(map[uint16]occurrence)
|
||||
for rootIndex, root := range page.Roots {
|
||||
if len(root.Children) > MaximumStageCount {
|
||||
return Page{}, ErrPageBlockInvalid
|
||||
}
|
||||
for childIndex, skillID := range root.Children {
|
||||
last[skillID] = occurrence{root: rootIndex, child: childIndex}
|
||||
}
|
||||
}
|
||||
out := Page{Index: page.Index, Roots: make([]Root, 0, len(page.Roots))}
|
||||
for rootIndex, root := range page.Roots {
|
||||
copyRoot := Root{SkillID: root.SkillID}
|
||||
for childIndex, skillID := range root.Children {
|
||||
if last[skillID] == (occurrence{root: rootIndex, child: childIndex}) {
|
||||
copyRoot.Children = append(copyRoot.Children, skillID)
|
||||
}
|
||||
}
|
||||
out.Roots = append(out.Roots, copyRoot)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func EncodePageBlock(page Page) ([]byte, error) {
|
||||
page, err := NormalizePage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := make([]byte, 0, 2+len(page.Roots)*3)
|
||||
body = append(body, page.Index, byte(len(page.Roots)))
|
||||
var scratch [2]byte
|
||||
for _, root := range page.Roots {
|
||||
binary.LittleEndian.PutUint16(scratch[:], root.SkillID)
|
||||
body = append(body, scratch[:]...)
|
||||
body = append(body, byte(len(root.Children)))
|
||||
for _, child := range root.Children {
|
||||
binary.LittleEndian.PutUint16(scratch[:], child)
|
||||
body = append(body, scratch[:]...)
|
||||
}
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// EncodeNotificationBody builds the current client's class-0/op447 combo
|
||||
// initialization body. applyAndEcho is deliberately zero: the server is
|
||||
// restoring durable state and must not make the client immediately echo an
|
||||
// op508 save while the character is still entering the scene.
|
||||
func EncodeNotificationBody(pages []Page) ([]byte, error) {
|
||||
if len(pages) == 0 || len(pages) > MaximumPageCount {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
ordered := append([]Page(nil), pages...)
|
||||
sort.Slice(ordered, func(left, right int) bool { return ordered[left].Index < ordered[right].Index })
|
||||
body := []byte{0, byte(len(ordered))}
|
||||
seen := make(map[byte]struct{}, len(ordered))
|
||||
for _, page := range ordered {
|
||||
if _, duplicate := seen[page.Index]; duplicate {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
seen[page.Index] = struct{}{}
|
||||
block, err := EncodePageBlock(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = append(body, block...)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// EncodePreloadNotificationBody builds the current client's class-0/op809
|
||||
// character-select preloader. Each entry owns its own page-count and the same
|
||||
// page blocks accepted by class-0/op447. The list is omitted by the bridge
|
||||
// when no Dark Knight roster row can be populated.
|
||||
func EncodePreloadNotificationBody(entries []PreloadEntry) ([]byte, error) {
|
||||
if len(entries) == 0 || len(entries) > maximumEntryCount {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
body := make([]byte, 0, 2)
|
||||
body = append(body, byte(len(entries)))
|
||||
seenSlots := make(map[byte]struct{}, len(entries))
|
||||
for _, entry := range entries {
|
||||
if _, duplicate := seenSlots[entry.Slot]; duplicate {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
seenSlots[entry.Slot] = struct{}{}
|
||||
if len(entry.Pages) == 0 || len(entry.Pages) > MaximumPageCount {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
ordered := append([]Page(nil), entry.Pages...)
|
||||
sort.Slice(ordered, func(left, right int) bool { return ordered[left].Index < ordered[right].Index })
|
||||
body = append(body, entry.Slot, 0, byte(len(ordered)))
|
||||
seenPages := make(map[byte]struct{}, len(ordered))
|
||||
for _, page := range ordered {
|
||||
if _, duplicate := seenPages[page.Index]; duplicate {
|
||||
return nil, ErrPageBlockInvalid
|
||||
}
|
||||
seenPages[page.Index] = struct{}{}
|
||||
block, err := EncodePageBlock(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = append(body, block...)
|
||||
}
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func RootSkillIDs(pages []Page) map[uint16]struct{} {
|
||||
result := make(map[uint16]struct{})
|
||||
for _, page := range pages {
|
||||
for _, root := range page.Roots {
|
||||
result[root.SkillID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ChildSkillIDs(page Page) map[uint16]struct{} {
|
||||
result := make(map[uint16]struct{})
|
||||
for _, root := range page.Roots {
|
||||
for _, child := range root.Children {
|
||||
result[child] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user