Files
90CNServer/server/internal/modules/dnf/joust/catalog.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

359 lines
9.5 KiB
Go

package joust
import (
"context"
"errors"
"fmt"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const EventPVFPath = "event/chn_event/chn_joust.evt"
var ErrPVFMalformed = errors.New("joust PVF is malformed")
// Rider is one immutable knight definition copied from the active Script.pvf.
// Win and Loss retain the four seven-value client battle timeline profiles;
// they are not the historical win/loss counters in op1241.
type Rider struct {
ID byte
AttackType byte
Name string
Win []uint16
Loss []uint16
}
type Catalog struct {
minimumLevel int
maximumBet uint32
rewardItemID int64
participationItemID int64
materialItemIDs []int64
riders []Rider
}
func LoadCatalog(ctx context.Context, source dnfpvf.Source) (*Catalog, error) {
if source == nil {
return nil, dnfpvf.ErrSourceRequired
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
text, err := source.ReadText(EventPVFPath)
if err != nil {
return nil, fmt.Errorf("read joust PVF %q: %w", EventPVFPath, err)
}
document, err := dnfpvf.Parse(EventPVFPath, text)
if err != nil {
return nil, fmt.Errorf("parse joust PVF %q: %w", EventPVFPath, err)
}
minimumLevel, err := joustSingletonInt(document, "min level")
if err != nil {
return nil, err
}
maximumBet, err := joustSingletonInt(document, "max betting")
if err != nil {
return nil, err
}
rewardItemID, err := joustSingletonInt(document, "reward")
if err != nil {
return nil, err
}
participationItemID, err := joustSingletonInt(document, "betting reward")
if err != nil {
return nil, err
}
materials, err := joustIntegerSection(document, "material")
if err != nil {
return nil, err
}
riders, err := joustRiders(document)
if err != nil {
return nil, err
}
if minimumLevel != MinimumLevel || maximumBet != MaximumBetPerRound ||
rewardItemID != PermanentCrystalID || participationItemID != ParticipationItemID ||
len(materials) != 2 || materials[0] != PermanentCrystalID || materials[1] != EventCrystalID {
return nil, fmt.Errorf(
"%w: incompatible rules min=%d max=%d reward=%d betting_reward=%d materials=%v",
ErrPVFMalformed,
minimumLevel,
maximumBet,
rewardItemID,
participationItemID,
materials,
)
}
if len(riders) < OpeningKnightCount {
return nil, fmt.Errorf("%w: riders=%d want-at-least=%d", ErrPVFMalformed, len(riders), OpeningKnightCount)
}
return &Catalog{
minimumLevel: int(minimumLevel),
maximumBet: uint32(maximumBet),
rewardItemID: rewardItemID,
participationItemID: participationItemID,
materialItemIDs: append([]int64(nil), materials...),
riders: cloneRiders(riders),
}, nil
}
func (c *Catalog) Riders() []Rider {
if c == nil {
return nil
}
return cloneRiders(c.riders)
}
func joustSingletonInt(document *dnfpvf.Document, name string) (int64, error) {
values, err := joustIntegerSection(document, name)
if err != nil {
return 0, err
}
if len(values) != 1 {
return 0, fmt.Errorf("%w: path=%s section=[%s] values=%d want=1", ErrPVFMalformed, EventPVFPath, name, len(values))
}
return values[0], nil
}
func joustIntegerSection(document *dnfpvf.Document, name string) ([]int64, error) {
if document == nil {
return nil, fmt.Errorf("%w: nil document", ErrPVFMalformed)
}
count := 0
for _, section := range document.Sections {
if strings.EqualFold(strings.TrimSpace(section.Name), name) {
count++
}
}
if count != 1 {
return nil, fmt.Errorf("%w: path=%s section=[%s] count=%d want=1", ErrPVFMalformed, EventPVFPath, name, count)
}
tokens, ok := document.Section(name)
if !ok {
return nil, fmt.Errorf("%w: path=%s section=[%s] unreadable", ErrPVFMalformed, EventPVFPath, name)
}
values := make([]int64, 0, len(tokens))
for _, token := range tokens {
if token.Kind != dnfpvf.TokenInt {
return nil, fmt.Errorf("%w: path=%s section=[%s] token=%q is not an integer", ErrPVFMalformed, EventPVFPath, name, token.Raw)
}
values = append(values, token.Int)
}
return values, nil
}
type joustRiderBuilder struct {
rider Rider
indexSet bool
attackTypeSet bool
nameSet bool
activeField string
}
func joustRiders(document *dnfpvf.Document) ([]Rider, error) {
if document == nil {
return nil, fmt.Errorf("%w: nil document", ErrPVFMalformed)
}
riders := make([]Rider, 0, 12)
seen := make(map[byte]struct{}, 12)
var current *joustRiderBuilder
for _, token := range document.Tokens {
if token.Kind == dnfpvf.TokenSection {
section := strings.ToLower(strings.TrimSpace(token.Value))
switch section {
case "knight":
if current != nil {
return nil, fmt.Errorf("%w: nested [knight] at line=%d", ErrPVFMalformed, token.Line)
}
current = &joustRiderBuilder{}
case "/knight":
if current == nil {
return nil, fmt.Errorf("%w: unmatched [/knight] at line=%d", ErrPVFMalformed, token.Line)
}
if err := current.validate(); err != nil {
return nil, err
}
if _, duplicate := seen[current.rider.ID]; duplicate {
return nil, fmt.Errorf("%w: duplicate rider index=%d", ErrPVFMalformed, current.rider.ID)
}
seen[current.rider.ID] = struct{}{}
riders = append(riders, cloneRider(current.rider))
current = nil
default:
if current != nil {
current.activeField = section
}
}
continue
}
if current == nil {
continue
}
if err := current.consume(token); err != nil {
return nil, err
}
}
if current != nil {
return nil, fmt.Errorf("%w: unterminated [knight]", ErrPVFMalformed)
}
return riders, nil
}
func (b *joustRiderBuilder) consume(token dnfpvf.Token) error {
if b == nil {
return fmt.Errorf("%w: nil rider builder", ErrPVFMalformed)
}
switch b.activeField {
case "index":
value, err := joustByteToken(token, "index")
if err != nil {
return err
}
if b.indexSet {
return fmt.Errorf("%w: duplicate rider index value", ErrPVFMalformed)
}
b.rider.ID = value
b.indexSet = true
case "attack type":
value, err := joustByteToken(token, "attack type")
if err != nil {
return err
}
if b.attackTypeSet {
return fmt.Errorf("%w: duplicate rider attack type", ErrPVFMalformed)
}
b.rider.AttackType = value
b.attackTypeSet = true
case "knight name":
if token.Kind != dnfpvf.TokenString || strings.TrimSpace(token.Value) == "" || b.nameSet {
return fmt.Errorf("%w: invalid rider name token=%q", ErrPVFMalformed, token.Raw)
}
b.rider.Name = token.Value
b.nameSet = true
case "win":
value, err := joustUint16Token(token, "win")
if err != nil {
return err
}
b.rider.Win = append(b.rider.Win, value)
case "loss":
value, err := joustUint16Token(token, "loss")
if err != nil {
return err
}
b.rider.Loss = append(b.rider.Loss, value)
}
return nil
}
func (b *joustRiderBuilder) validate() error {
if b == nil || !b.indexSet || !b.attackTypeSet || !b.nameSet || len(b.rider.Win) != 28 || len(b.rider.Loss) != 28 {
return fmt.Errorf(
"%w: rider index_set=%t attack_set=%t name_set=%t win=%d loss=%d",
ErrPVFMalformed,
b != nil && b.indexSet,
b != nil && b.attackTypeSet,
b != nil && b.nameSet,
func() int {
if b == nil {
return 0
}
return len(b.rider.Win)
}(),
func() int {
if b == nil {
return 0
}
return len(b.rider.Loss)
}(),
)
}
if _, ok := attackTypeTableOffset(b.rider.AttackType); !ok {
return fmt.Errorf("%w: rider=%d unsupported attack type=%d", ErrPVFMalformed, b.rider.ID, b.rider.AttackType)
}
if err := validateBattleTimelineProfiles(b.rider.ID, "win", b.rider.Win); err != nil {
return err
}
if err := validateBattleTimelineProfiles(b.rider.ID, "loss", b.rider.Loss); err != nil {
return err
}
return nil
}
func validateBattleTimelineProfiles(rider byte, field string, values []uint16) error {
for group := 0; group < 4; group++ {
previous := uint16(0)
for position := 0; position < 7; position++ {
value := values[group*7+position]
if value < previous || value > 100 {
return fmt.Errorf("%w: rider=%d %s group=%d position=%d timeline=%d previous=%d", ErrPVFMalformed, rider, field, group, position, value, previous)
}
previous = value
}
if previous == 0 {
return fmt.Errorf("%w: rider=%d %s group=%d terminal timeline is zero", ErrPVFMalformed, rider, field, group)
}
}
return nil
}
func attackTypeTableOffset(attackType byte) (int, bool) {
switch attackType {
case 0:
return 0, true
case 1:
return 7, true
case 27:
return 14, true
case 28:
return 21, true
default:
return 0, false
}
}
func (c *Catalog) rider(id byte) (Rider, bool) {
if c == nil {
return Rider{}, false
}
for _, rider := range c.riders {
if rider.ID == id {
return cloneRider(rider), true
}
}
return Rider{}, false
}
func joustByteToken(token dnfpvf.Token, field string) (byte, error) {
if token.Kind != dnfpvf.TokenInt || token.Int < 0 || token.Int > 255 {
return 0, fmt.Errorf("%w: rider %s token=%q", ErrPVFMalformed, field, token.Raw)
}
return byte(token.Int), nil
}
func joustUint16Token(token dnfpvf.Token, field string) (uint16, error) {
if token.Kind != dnfpvf.TokenInt || token.Int < 0 || token.Int > 65535 {
return 0, fmt.Errorf("%w: rider %s token=%q", ErrPVFMalformed, field, token.Raw)
}
return uint16(token.Int), nil
}
func cloneRiders(source []Rider) []Rider {
out := make([]Rider, len(source))
for index := range source {
out[index] = cloneRider(source[index])
}
return out
}
func cloneRider(source Rider) Rider {
source.Win = append([]uint16(nil), source.Win...)
source.Loss = append([]uint16(nil), source.Loss...)
return source
}