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,286 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
currentChronicleCanonicalAuraBase = 1254
|
||||
currentChronicleFailureFragmentItemID = 3311
|
||||
)
|
||||
|
||||
func (s *Service) alignedChronicleRefineResolverForCommand(opcode dnfenum.CmdPacket) (alignedcmd.ChronicleRefineResolver, error) {
|
||||
if opcode != dnfenum.CmdPacketEnchant3rdChronicleItem {
|
||||
return nil, nil
|
||||
}
|
||||
catalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if catalog == nil {
|
||||
return nil, errDungeonDropSourceRequired
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
source, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(materialItemID, targetItemID int64, characterJob, firstGrowType, optionNo byte) (alignedcmd.ChronicleRefineResolution, error) {
|
||||
return resolveCurrentChronicleRefineMetadata(catalog, source, materialItemID, targetItemID, characterJob, firstGrowType, optionNo)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveCurrentChronicleRefineMetadata(
|
||||
catalog *pvfDungeonDropCatalog,
|
||||
source dnfpvf.Source,
|
||||
materialItemID int64,
|
||||
targetItemID int64,
|
||||
characterJob byte,
|
||||
firstGrowType byte,
|
||||
optionNo byte,
|
||||
) (alignedcmd.ChronicleRefineResolution, error) {
|
||||
resolution := alignedcmd.ChronicleRefineResolution{}
|
||||
if catalog == nil || source == nil {
|
||||
return resolution, errDungeonDropSourceRequired
|
||||
}
|
||||
if materialItemID <= 0 || materialItemID > math.MaxUint32 || targetItemID <= 0 || targetItemID > math.MaxUint32 {
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
material, err := catalog.ResolveItem(uint32(materialItemID))
|
||||
if err != nil {
|
||||
if errors.Is(err, errDungeonDropItemUnresolved) {
|
||||
return resolution, nil
|
||||
}
|
||||
return resolution, err
|
||||
}
|
||||
if material.Kind != dungeonDropItemStackable {
|
||||
return resolution, nil
|
||||
}
|
||||
materialDocument, err := parseDungeonCardPVFDocument(source, material.PVFPath)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("parse chronicle material item=%d path=%s: %w", materialItemID, material.PVFPath, err)
|
||||
}
|
||||
importScript, found := materialDocument.Text("import script")
|
||||
if !found || strings.TrimSpace(importScript) == "" {
|
||||
return resolution, nil
|
||||
}
|
||||
auraType, found := materialDocument.Int("type")
|
||||
if !found || auraType < 0 || auraType > 2 {
|
||||
return resolution, nil
|
||||
}
|
||||
probabilities := materialDocument.Ints("probability")
|
||||
if len(probabilities) < 2 || probabilities[0] < 0 || probabilities[0] > 100 || probabilities[1] < 0 || probabilities[1] > 100 {
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
target, err := catalog.ResolveItem(uint32(targetItemID))
|
||||
if err != nil {
|
||||
if errors.Is(err, errDungeonDropItemUnresolved) {
|
||||
return resolution, nil
|
||||
}
|
||||
return resolution, err
|
||||
}
|
||||
if target.Kind != dungeonDropItemEquipment || target.Rarity != 5 {
|
||||
return resolution, nil
|
||||
}
|
||||
targetType, targetTypeCode, ok := currentChronicleEquipmentType(target.EquipmentType)
|
||||
if !ok {
|
||||
return resolution, nil
|
||||
}
|
||||
targetDocument, err := parseDungeonCardPVFDocument(source, target.PVFPath)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("parse chronicle target item=%d path=%s: %w", targetItemID, target.PVFPath, err)
|
||||
}
|
||||
if !currentChronicleCharacterItemCheckMatches(targetDocument, characterJob, firstGrowType, targetType) {
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
importedPath, importedText, err := readDungeonDropText(source, "stackable", importScript)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("read chronicle import script=%s: %w", importScript, err)
|
||||
}
|
||||
importedDocument, err := dnfpvf.Parse(importedPath, importedText)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("parse chronicle import script=%s: %w", importedPath, err)
|
||||
}
|
||||
if !currentChronicleImportedOptionMatches(importedDocument, characterJob, firstGrowType, targetType, optionNo) {
|
||||
return resolution, nil
|
||||
}
|
||||
disjointConfig, err := loadCurrentDisjointPVFConfig(source)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("load chronicle failure disjoint config: %w", err)
|
||||
}
|
||||
cubeReward, _, _, err := currentEquipmentDisjointCubeReward(disjointConfig, targetDocument)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("resolve chronicle failure cube target=%d path=%s: %w", targetItemID, target.PVFPath, err)
|
||||
}
|
||||
cubeItem, err := resolveCurrentMagicBoxRewardItem(catalog, source, int64(cubeReward.ItemID))
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("resolve chronicle failure cube item=%d: %w", cubeReward.ItemID, err)
|
||||
}
|
||||
needMaterial := materialDocument.Ints("need material")
|
||||
if len(needMaterial) < 1 || needMaterial[0] != currentChronicleFailureFragmentItemID {
|
||||
return resolution, nil
|
||||
}
|
||||
fragmentItem, err := resolveCurrentMagicBoxRewardItem(catalog, source, currentChronicleFailureFragmentItemID)
|
||||
if err != nil {
|
||||
return resolution, fmt.Errorf("resolve chronicle failure fragment item=%d: %w", currentChronicleFailureFragmentItemID, err)
|
||||
}
|
||||
resolution.MaterialPVFPath = material.PVFPath
|
||||
resolution.ImportedScriptPVFPath = importedPath
|
||||
resolution.AuraType = byte(auraType)
|
||||
resolution.CanonicalAuraOptionID = int32(currentChronicleCanonicalAuraBase + auraType)
|
||||
resolution.Probabilities = [2]byte{byte(probabilities[0]), byte(probabilities[1])}
|
||||
resolution.FailureFragmentItemID = currentChronicleFailureFragmentItemID
|
||||
resolution.FailureCubeCount = int64(cubeReward.Count)
|
||||
resolution.FailureCubeItem = cubeItem
|
||||
resolution.FailureFragmentItem = fragmentItem
|
||||
resolution.TargetPVFPath = target.PVFPath
|
||||
resolution.TargetKind = string(target.Kind)
|
||||
resolution.TargetEquipmentType = targetType
|
||||
resolution.TargetEquipmentTypeCode = targetTypeCode
|
||||
resolution.TargetDurability = target.Durability
|
||||
resolution.Matched = true
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
func currentChronicleCharacterItemCheckMatches(document *dnfpvf.Document, job, grow byte, equipmentType string) bool {
|
||||
if document == nil {
|
||||
return false
|
||||
}
|
||||
for _, section := range document.Sections {
|
||||
if !strings.EqualFold(strings.TrimSpace(section.Name), "character item check") || section.Start < 0 || section.End > len(document.Tokens) {
|
||||
continue
|
||||
}
|
||||
if currentChronicleCheckTupleMatches(document.Tokens[section.Start:section.End], job, grow, equipmentType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func currentChronicleImportedOptionMatches(document *dnfpvf.Document, job, grow byte, equipmentType string, optionNo byte) bool {
|
||||
if document == nil {
|
||||
return false
|
||||
}
|
||||
inEnchant := false
|
||||
matchingCheck := false
|
||||
for _, section := range document.Sections {
|
||||
name := strings.ToLower(strings.TrimSpace(section.Name))
|
||||
switch name {
|
||||
case "3choro enchant":
|
||||
inEnchant = true
|
||||
matchingCheck = false
|
||||
case "/3choro enchant":
|
||||
return false
|
||||
case "check":
|
||||
matchingCheck = false
|
||||
if !inEnchant || section.Start < 0 || section.End > len(document.Tokens) {
|
||||
continue
|
||||
}
|
||||
matchingCheck = currentChronicleCheckTupleMatches(
|
||||
document.Tokens[section.Start:section.End],
|
||||
job,
|
||||
grow,
|
||||
equipmentType,
|
||||
)
|
||||
case "/check":
|
||||
matchingCheck = false
|
||||
case "skill":
|
||||
if !inEnchant || !matchingCheck || section.Start < 0 || section.End > len(document.Tokens) {
|
||||
continue
|
||||
}
|
||||
ints, _ := currentChronicleSectionValues(document.Tokens[section.Start:section.End])
|
||||
if len(ints) >= 1 && ints[0] == int64(optionNo) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// currentChronicleCheckTupleMatches preserves the PVF value order while
|
||||
// walking one [check] or [character item check] section. Shared Chronicle
|
||||
// equipment stores more than one compatibility tuple in the same section,
|
||||
// for example male/female gunner as:
|
||||
//
|
||||
// 2 1 `weapon` 5 1 `weapon`
|
||||
//
|
||||
// Splitting all integers from all text values loses those tuple boundaries
|
||||
// and only validates the first profession. The current client evaluates every
|
||||
// (job, first-grow, equipment-type) tuple in source order.
|
||||
func currentChronicleCheckTupleMatches(tokens []dnfpvf.Token, job, grow byte, equipmentType string) bool {
|
||||
for index := 0; index+2 < len(tokens); index++ {
|
||||
if tokens[index].Kind != dnfpvf.TokenInt || tokens[index].Int != int64(job) ||
|
||||
tokens[index+1].Kind != dnfpvf.TokenInt || tokens[index+1].Int != int64(grow) {
|
||||
continue
|
||||
}
|
||||
typeToken := tokens[index+2]
|
||||
if typeToken.Kind != dnfpvf.TokenString && typeToken.Kind != dnfpvf.TokenIdent {
|
||||
continue
|
||||
}
|
||||
if currentChronicleNormalizeType(typeToken.Value) == equipmentType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func currentChronicleSectionValues(tokens []dnfpvf.Token) ([]int64, []string) {
|
||||
ints := make([]int64, 0, len(tokens))
|
||||
texts := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
ints = append(ints, token.Int)
|
||||
case dnfpvf.TokenString, dnfpvf.TokenIdent:
|
||||
texts = append(texts, token.Value)
|
||||
}
|
||||
}
|
||||
return ints, texts
|
||||
}
|
||||
|
||||
func currentChronicleEquipmentType(raw string) (string, byte, bool) {
|
||||
typeName := currentChronicleNormalizeType(raw)
|
||||
switch typeName {
|
||||
case "weapon":
|
||||
return typeName, 12, true
|
||||
case "coat":
|
||||
return typeName, 14, true
|
||||
case "shoulder":
|
||||
return typeName, 15, true
|
||||
case "pants":
|
||||
return typeName, 16, true
|
||||
case "shoes":
|
||||
return typeName, 17, true
|
||||
case "waist":
|
||||
return typeName, 18, true
|
||||
case "amulet":
|
||||
return typeName, 19, true
|
||||
case "wrist":
|
||||
return typeName, 20, true
|
||||
case "ring":
|
||||
return typeName, 21, true
|
||||
case "support":
|
||||
return typeName, 22, true
|
||||
case "magic stone":
|
||||
return typeName, 23, true
|
||||
case "earring":
|
||||
return typeName, 25, true
|
||||
default:
|
||||
return typeName, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func currentChronicleNormalizeType(raw string) string {
|
||||
return strings.ToLower(strings.TrimSpace(strings.Trim(strings.TrimSpace(strings.ReplaceAll(raw, "`", "")), "[]")))
|
||||
}
|
||||
Reference in new issue
Block a user