Files
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

106 lines
3.4 KiB
Go

// Package profession owns the PVF-backed character advancement state machine.
// It contains no network opcodes and performs no repository I/O.
package profession
import (
"errors"
"fmt"
"math"
"strings"
)
var (
ErrRewardUnsupported = errors.New("dnf profession reward type is unsupported")
ErrRewardMalformed = errors.New("dnf profession reward data is malformed")
ErrTransitionInvalid = errors.New("dnf profession transition is invalid")
ErrTransitionOutOfStep = errors.New("dnf profession transition is out of sequence")
)
type Kind uint8
const (
KindNone Kind = iota
KindClassChange
KindAwakening
)
// Transition is the detached result of applying one final profession quest
// reward to the packed grow_type byte. Low four bits select the class branch;
// high four bits select the awakening stage.
type Transition struct {
Kind Kind
ChainType byte
GrowNumber byte
PreviousGrowType byte
NewGrowType byte
FirstGrowType byte
AwakeningStage byte
}
// Request is the detached profession reward encoded by one quest definition.
// It contains no character state; the repository owner must resolve it through
// the job's PVF profile inside the same transaction as the quest completion.
type Request struct {
Kind Kind
ChainType byte
GrowNumber byte
JobChangeQuest int
}
func Decode(growType byte) (first byte, awakening byte) {
return growType & 0x0f, (growType >> 4) & 0x0f
}
func Encode(first byte, awakening byte) (byte, error) {
if first > 0x0f || awakening > 0x0f {
return 0, fmt.Errorf("%w: first=%d awakening=%d", ErrTransitionInvalid, first, awakening)
}
return awakening<<4 | first, nil
}
// ParseReward accepts only the two profession reward tags used by the runtime
// PVF. It deliberately does not inspect or change grow_type.
func ParseReward(jobChangeQuest int, rewardType string, rewardData []int64) (Request, error) {
tag := normalizeTag(rewardType)
if tag != "grow type" && tag != "awakening type" {
return Request{}, fmt.Errorf("%w: type=%q", ErrRewardUnsupported, rewardType)
}
if len(rewardData) == 0 || rewardData[0] <= 0 || rewardData[0] > math.MaxUint8 {
return Request{}, fmt.Errorf("%w: type=%q values=%v", ErrRewardMalformed, rewardType, rewardData)
}
value := byte(rewardData[0])
switch tag {
case "grow type":
if value > 0x0f {
return Request{}, fmt.Errorf("%w: class=%d", ErrTransitionInvalid, value)
}
if jobChangeQuest != 0 && jobChangeQuest != 1 {
return Request{}, fmt.Errorf("%w: class change quest marker=%d", ErrTransitionInvalid, jobChangeQuest)
}
return Request{
Kind: KindClassChange, ChainType: 1, GrowNumber: value, JobChangeQuest: jobChangeQuest,
}, nil
case "awakening type":
if value > 2 {
return Request{}, fmt.Errorf("%w: awakening target=%d", ErrTransitionInvalid, value)
}
if jobChangeQuest != 0 && jobChangeQuest != int(value)+1 {
return Request{}, fmt.Errorf("%w: awakening target=%d quest marker=%d", ErrTransitionInvalid, value, jobChangeQuest)
}
return Request{
Kind: KindAwakening, ChainType: 2, GrowNumber: value, JobChangeQuest: jobChangeQuest,
}, nil
default:
return Request{}, fmt.Errorf("%w: type=%q", ErrRewardUnsupported, rewardType)
}
}
func normalizeTag(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if len(value) >= 2 && value[0] == '[' && value[len(value)-1] == ']' {
value = strings.TrimSpace(value[1 : len(value)-1])
}
return value
}