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,296 @@
|
||||
package pet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
petEquipmentListPath = "equipment/equipment.lst"
|
||||
petCreatureExperiencePath = "creature/exptable.tbl"
|
||||
MaxCreatureLevel = 50
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPetPVFSourceRequired = errors.New("pet PVF source is required")
|
||||
ErrPetPVFEquipmentUnresolved = errors.New("pet equipment is not present in equipment.lst")
|
||||
ErrPetPVFNotCreature = errors.New("pet equipment type is not creature")
|
||||
ErrPetPVFHatchOutputInvalid = errors.New("pet hatch output index is invalid")
|
||||
ErrPetPVFExperienceTableShape = errors.New("pet creature experience table is invalid")
|
||||
)
|
||||
|
||||
// PetHatchDefinition is the typed, PVF-proven egg-to-creature mapping used by
|
||||
// the hatch owner. It contains domain data only and has no packet fields.
|
||||
type PetHatchDefinition struct {
|
||||
EggItemID int64
|
||||
HatchedItemID int64
|
||||
EggPVFPath string
|
||||
HatchedPVFPath string
|
||||
// MinimumLevel is retained as typed output-item metadata. The 86JP hatch
|
||||
// transaction still creates a new creature instance at level 1.
|
||||
MinimumLevel int
|
||||
}
|
||||
|
||||
// PetHatchResolver lets the mutation owner depend on a real PVF mapping rather
|
||||
// than inventory Extra fields or request-body item-id guesses.
|
||||
type PetHatchResolver interface {
|
||||
ResolveHatch(eggItemID int64) (PetHatchDefinition, error)
|
||||
}
|
||||
|
||||
// PetCreatureDefinition proves that an equipment-list item is a current
|
||||
// runtime-PVF creature. It is deliberately small: growth persistence needs an
|
||||
// item-kind authority, not protocol fields or an inferred slot convention.
|
||||
type PetCreatureDefinition struct {
|
||||
ItemID int64
|
||||
PVFPath string
|
||||
Name string
|
||||
}
|
||||
|
||||
// PetCreatureResolver is the fail-closed item-kind boundary used before any
|
||||
// equipped creature growth or satiety mutation. Slot 24 is also a valid
|
||||
// ordinary support-weapon slot, so position and raw instance bytes alone are
|
||||
// not sufficient proof that an item is a creature.
|
||||
type PetCreatureResolver interface {
|
||||
ResolveCreature(itemID int64) (PetCreatureDefinition, error)
|
||||
}
|
||||
|
||||
// CreatureExperienceTable preserves the 50 values from
|
||||
// creature/exptable.tbl. Current pet progression is capped at level 50, so
|
||||
// LevelForExperience consumes the first 49 thresholds exactly like 86JP.
|
||||
type CreatureExperienceTable struct {
|
||||
thresholds [MaxCreatureLevel]int64
|
||||
}
|
||||
|
||||
// Thresholds returns a copy of the authoritative PVF values.
|
||||
func (t CreatureExperienceTable) Thresholds() []int64 {
|
||||
out := make([]int64, len(t.thresholds))
|
||||
copy(out, t.thresholds[:])
|
||||
return out
|
||||
}
|
||||
|
||||
// LevelForExperience maps cumulative creature experience to level 1..50.
|
||||
func (t CreatureExperienceTable) LevelForExperience(experience int64) int {
|
||||
if experience < 0 {
|
||||
experience = 0
|
||||
}
|
||||
level := 1
|
||||
for nextLevel := 2; nextLevel <= MaxCreatureLevel; nextLevel++ {
|
||||
if t.thresholds[nextLevel-2] > experience {
|
||||
break
|
||||
}
|
||||
level = nextLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// PVFCatalog owns immutable pet item mappings and the creature experience
|
||||
// table loaded from the runtime Script.pvf.
|
||||
type PVFCatalog struct {
|
||||
source dnfpvf.Source
|
||||
equipmentPaths map[int64]string
|
||||
experience CreatureExperienceTable
|
||||
|
||||
evolutionMu sync.Mutex
|
||||
evolutionLoaded bool
|
||||
evolutionIndex petEvolutionIndex
|
||||
evolutionErr error
|
||||
}
|
||||
|
||||
// NewPVFCatalog loads only the authoritative indexes needed for the first pet
|
||||
// domain closure: equipment item references and creature experience values.
|
||||
func NewPVFCatalog(source dnfpvf.Source) (*PVFCatalog, error) {
|
||||
if source == nil {
|
||||
return nil, ErrPetPVFSourceRequired
|
||||
}
|
||||
equipmentPaths, err := loadPetEquipmentPaths(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
experience, err := loadCreatureExperienceTable(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PVFCatalog{
|
||||
source: source,
|
||||
equipmentPaths: equipmentPaths,
|
||||
experience: experience,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExperienceTable returns the immutable-by-value creature experience table.
|
||||
func (c *PVFCatalog) ExperienceTable() CreatureExperienceTable {
|
||||
if c == nil {
|
||||
return CreatureExperienceTable{}
|
||||
}
|
||||
return c.experience
|
||||
}
|
||||
|
||||
// ResolveCreature independently verifies the current item against the active
|
||||
// equipment.lst and its equipment document. The same typed proof is reused by
|
||||
// hatch/evolution code, while callers cannot inject a request-body item kind.
|
||||
func (c *PVFCatalog) ResolveCreature(itemID int64) (PetCreatureDefinition, error) {
|
||||
if c == nil || c.source == nil {
|
||||
return PetCreatureDefinition{}, ErrPetPVFSourceRequired
|
||||
}
|
||||
documentPath, document, err := c.resolveCreatureEquipment(itemID)
|
||||
if err != nil {
|
||||
return PetCreatureDefinition{}, err
|
||||
}
|
||||
name, _ := document.Text("name")
|
||||
return PetCreatureDefinition{
|
||||
ItemID: itemID,
|
||||
PVFPath: documentPath,
|
||||
Name: strings.TrimSpace(name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResolveHatch applies the 86JP domain rule to current runtime PVF data: the
|
||||
// source must be [creature] equipment and have a positive [output index]
|
||||
// different from itself. The output is independently resolved and verified as
|
||||
// creature equipment before the mapping can authorize a mutation.
|
||||
func (c *PVFCatalog) ResolveHatch(eggItemID int64) (PetHatchDefinition, error) {
|
||||
if c == nil || c.source == nil {
|
||||
return PetHatchDefinition{}, ErrPetPVFSourceRequired
|
||||
}
|
||||
eggPath, eggDocument, err := c.resolveCreatureEquipment(eggItemID)
|
||||
if err != nil {
|
||||
return PetHatchDefinition{}, err
|
||||
}
|
||||
outputItemID, found := eggDocument.Int("output index")
|
||||
if !found || outputItemID <= 0 || outputItemID > math.MaxUint32 || outputItemID == eggItemID {
|
||||
return PetHatchDefinition{}, fmt.Errorf("%w: item_id=%d output_index=%d", ErrPetPVFHatchOutputInvalid, eggItemID, outputItemID)
|
||||
}
|
||||
hatchedPath, hatchedDocument, err := c.resolveCreatureEquipment(outputItemID)
|
||||
if err != nil {
|
||||
return PetHatchDefinition{}, fmt.Errorf("resolve pet hatch output item_id=%d: %w", outputItemID, err)
|
||||
}
|
||||
minimumLevel := 1
|
||||
if value, found := hatchedDocument.Int("minimum level"); found {
|
||||
if value <= 0 || value > MaxCreatureLevel {
|
||||
return PetHatchDefinition{}, fmt.Errorf("%w: output item_id=%d minimum_level=%d", ErrPetPVFHatchOutputInvalid, outputItemID, value)
|
||||
}
|
||||
minimumLevel = int(value)
|
||||
}
|
||||
return PetHatchDefinition{
|
||||
EggItemID: eggItemID,
|
||||
HatchedItemID: outputItemID,
|
||||
EggPVFPath: eggPath,
|
||||
HatchedPVFPath: hatchedPath,
|
||||
MinimumLevel: minimumLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PVFCatalog) resolveCreatureEquipment(itemID int64) (string, *dnfpvf.Document, error) {
|
||||
if itemID <= 0 || itemID > math.MaxUint32 {
|
||||
return "", nil, fmt.Errorf("%w: item_id=%d", ErrPetPVFEquipmentUnresolved, itemID)
|
||||
}
|
||||
listedPath, found := c.equipmentPaths[itemID]
|
||||
if !found {
|
||||
return "", nil, fmt.Errorf("%w: item_id=%d", ErrPetPVFEquipmentUnresolved, itemID)
|
||||
}
|
||||
documentPath, text, err := readPetPVFReference(c.source, "equipment", listedPath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("read pet equipment item_id=%d path=%s: %w", itemID, listedPath, err)
|
||||
}
|
||||
document, err := dnfpvf.Parse(documentPath, text)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse pet equipment item_id=%d path=%s: %w", itemID, documentPath, err)
|
||||
}
|
||||
equipmentType, found := document.Text("equipment type")
|
||||
if !found || !strings.Contains(normalizePetPVFType(equipmentType), "[creature]") {
|
||||
return "", nil, fmt.Errorf("%w: item_id=%d equipment_type=%q", ErrPetPVFNotCreature, itemID, equipmentType)
|
||||
}
|
||||
return documentPath, document, nil
|
||||
}
|
||||
|
||||
func loadPetEquipmentPaths(source dnfpvf.Source) (map[int64]string, error) {
|
||||
text, err := source.ReadText(petEquipmentListPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", petEquipmentListPath, err)
|
||||
}
|
||||
document, err := dnfpvf.Parse(petEquipmentListPath, text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", petEquipmentListPath, err)
|
||||
}
|
||||
entries := dnfpvf.ParseList(document)
|
||||
paths := make(map[int64]string, len(entries))
|
||||
for _, entry := range entries {
|
||||
listedPath := cleanPetPVFPath(entry.Path)
|
||||
if entry.ID <= 0 || entry.ID > math.MaxUint32 || listedPath == "" {
|
||||
continue
|
||||
}
|
||||
if previous, exists := paths[entry.ID]; exists && !strings.EqualFold(previous, listedPath) {
|
||||
return nil, fmt.Errorf("parse %s: duplicate item_id=%d paths=%q,%q", petEquipmentListPath, entry.ID, previous, listedPath)
|
||||
}
|
||||
paths[entry.ID] = listedPath
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return nil, fmt.Errorf("parse %s: no valid item references", petEquipmentListPath)
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func loadCreatureExperienceTable(source dnfpvf.Source) (CreatureExperienceTable, error) {
|
||||
if source == nil {
|
||||
return CreatureExperienceTable{}, ErrPetPVFSourceRequired
|
||||
}
|
||||
text, err := source.ReadText(petCreatureExperiencePath)
|
||||
if err != nil {
|
||||
return CreatureExperienceTable{}, fmt.Errorf("read %s: %w", petCreatureExperiencePath, err)
|
||||
}
|
||||
document, err := dnfpvf.Parse(petCreatureExperiencePath, text)
|
||||
if err != nil {
|
||||
return CreatureExperienceTable{}, fmt.Errorf("parse %s: %w", petCreatureExperiencePath, err)
|
||||
}
|
||||
values := make([]int64, 0, MaxCreatureLevel)
|
||||
for _, token := range document.Tokens {
|
||||
if token.Kind == dnfpvf.TokenInt {
|
||||
values = append(values, token.Int)
|
||||
}
|
||||
}
|
||||
if len(values) < MaxCreatureLevel {
|
||||
return CreatureExperienceTable{}, fmt.Errorf("%w: path=%s values=%d want_at_least=%d", ErrPetPVFExperienceTableShape, petCreatureExperiencePath, len(values), MaxCreatureLevel)
|
||||
}
|
||||
var table CreatureExperienceTable
|
||||
for index, value := range values[:MaxCreatureLevel] {
|
||||
if value <= 0 || (index > 0 && value <= values[index-1]) {
|
||||
return CreatureExperienceTable{}, fmt.Errorf("%w: path=%s index=%d value=%d", ErrPetPVFExperienceTableShape, petCreatureExperiencePath, index, value)
|
||||
}
|
||||
table.thresholds[index] = value
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func readPetPVFReference(source dnfpvf.Source, root, listedPath string) (string, string, error) {
|
||||
clean := cleanPetPVFPath(listedPath)
|
||||
candidates := []string{clean}
|
||||
root = cleanPetPVFPath(root)
|
||||
if root != "" && !strings.HasPrefix(strings.ToLower(clean), strings.ToLower(root)+"/") {
|
||||
candidates = append(candidates, path.Join(root, clean))
|
||||
}
|
||||
var lastErr error
|
||||
for _, candidate := range candidates {
|
||||
text, err := source.ReadText(candidate)
|
||||
if err == nil {
|
||||
return candidate, text, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
return "", "", lastErr
|
||||
}
|
||||
|
||||
func cleanPetPVFPath(value string) string {
|
||||
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
|
||||
value = strings.TrimPrefix(value, "./")
|
||||
return strings.TrimPrefix(value, "/")
|
||||
}
|
||||
|
||||
func normalizePetPVFType(value string) string {
|
||||
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(strings.ReplaceAll(value, "`", ""))), " "))
|
||||
}
|
||||
Reference in new issue
Block a user