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,358 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user