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,402 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
currentForgingSuccessBase = 10000
|
||||
// Current NoPack maps raw+0x57 to the encoded equipment-state field at
|
||||
// +0x78, which AppendWeaponForgingTooltipLines uses as its level value.
|
||||
currentForgingRawOffset = 0x57
|
||||
)
|
||||
|
||||
// ForgingResult is the durable outcome of one current-client op438 attempt.
|
||||
// Success means the request was accepted and material was consumed; a chance
|
||||
// failure keeps the old level while still returning Success=true.
|
||||
type ForgingResult struct {
|
||||
CharacterID string
|
||||
Success bool
|
||||
UpgradeSucceeded bool
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
TargetItemTemplateID int32
|
||||
MaterialSlotIndex int16
|
||||
MaterialItemID int64
|
||||
MaterialCost int64
|
||||
MaterialRemainingCount int64
|
||||
OldLevel byte
|
||||
NewLevel byte
|
||||
ReinforceLevel byte
|
||||
IndependentAttack int64
|
||||
SuccessWeight int
|
||||
TargetUpdatedStack dnfrepo.ItemStack
|
||||
MaterialUpdatedStack dnfrepo.ItemStack
|
||||
Changed bool
|
||||
}
|
||||
|
||||
// Forge validates and commits a PVF-backed weapon-forging attempt. Target and
|
||||
// material changes share one character-item transaction for both list 0 and 3.
|
||||
func (o *Owner) Forge(ctx context.Context, cmd Command, resolver alignedcmd.ForgingResolver) (ForgingResult, error) {
|
||||
base := ForgingResult{
|
||||
TargetListType: cmd.TargetListType,
|
||||
TargetSlotIndex: cmd.TargetSlotIndex,
|
||||
TargetItemTemplateID: cmd.TargetItemTemplateID,
|
||||
MaterialSlotIndex: cmd.MaterialSlotIndex,
|
||||
}
|
||||
if o == nil || o.repo == nil || o.equipment == nil {
|
||||
return base, ErrOwnerUnavailable
|
||||
}
|
||||
if cmd.SelectedCharacterID == 0 {
|
||||
return base, ErrCharacterRequired
|
||||
}
|
||||
if resolver == nil {
|
||||
return base, fmt.Errorf("forging runtime-PVF resolver required")
|
||||
}
|
||||
if cmd.TargetListType != listTypeMain && cmd.TargetListType != listTypeEquipment {
|
||||
return base, nil
|
||||
}
|
||||
if cmd.TargetSlotIndex < 0 || cmd.MaterialSlotIndex < 0 || dnfrepo.IsAccountSharedInventorySlot(listTypeMain, cmd.MaterialSlotIndex) {
|
||||
return base, nil
|
||||
}
|
||||
if cmd.TargetListType == listTypeMain && cmd.TargetSlotIndex == cmd.MaterialSlotIndex {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
characterID := strconv.FormatUint(uint64(cmd.SelectedCharacterID), 10)
|
||||
base.CharacterID = characterID
|
||||
if !o.inItemTransaction {
|
||||
var result ForgingResult
|
||||
err := o.withinInventoryTransaction(ctx, characterID, func(txOwner *Owner) error {
|
||||
var innerErr error
|
||||
result, innerErr = txOwner.Forge(ctx, cmd, resolver)
|
||||
return innerErr
|
||||
})
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
if result.CharacterID == "" {
|
||||
result = base
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
inventoryRecord, found, err := o.repo.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
if !found {
|
||||
return base, ErrInventoryNotFound
|
||||
}
|
||||
inventoryRecord = dnfrepo.CloneInventory(inventoryRecord)
|
||||
if inventoryRecord.Slots == nil {
|
||||
inventoryRecord.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
|
||||
var target dnfrepo.ItemStack
|
||||
var equipmentRecord dnfrepo.EquipmentRecord
|
||||
var equipmentEntry dnfrepo.EquipmentEntry
|
||||
var targetKey string
|
||||
switch cmd.TargetListType {
|
||||
case listTypeMain:
|
||||
targetKey = slotKey(listTypeMain, cmd.TargetSlotIndex)
|
||||
stack, ok := inventoryRecord.Slots[targetKey]
|
||||
if !ok || stack.ItemID != int64(cmd.TargetItemTemplateID) || stack.Count <= 0 {
|
||||
return base, nil
|
||||
}
|
||||
target = cloneStack(stack)
|
||||
case listTypeEquipment:
|
||||
loaded, ok, loadErr := o.equipment.Load(ctx, characterID)
|
||||
if loadErr != nil {
|
||||
return base, loadErr
|
||||
}
|
||||
if !ok {
|
||||
return base, nil
|
||||
}
|
||||
equipmentRecord = dnfrepo.CloneEquipment(loaded)
|
||||
targetKey = strconv.FormatInt(int64(cmd.TargetSlotIndex), 10)
|
||||
entry, ok := equipmentRecord.Entries[targetKey]
|
||||
if !ok || entry.ItemID != int64(cmd.TargetItemTemplateID) {
|
||||
return base, nil
|
||||
}
|
||||
equipmentEntry = entry
|
||||
target = forgingStackFromEquipmentEntry(entry)
|
||||
}
|
||||
|
||||
if isEquipmentLocked(target) {
|
||||
return base, nil
|
||||
}
|
||||
if !equipmentStackCountIsSingle(target) {
|
||||
// 一份锻造材料只能作用一件装备:历史堆叠行(装备 Count>1)一律拒绝。
|
||||
return base, nil
|
||||
}
|
||||
oldLevel := forgingLevelOf(target)
|
||||
base.OldLevel = oldLevel
|
||||
base.NewLevel = oldLevel
|
||||
base.ReinforceLevel = upgradeLevelOf(target)
|
||||
materialKey := slotKey(listTypeMain, cmd.MaterialSlotIndex)
|
||||
material, ok := inventoryRecord.Slots[materialKey]
|
||||
if !ok || material.ItemID <= 0 || material.Count <= 0 ||
|
||||
(!material.ExpireAt.IsZero() && !material.ExpireAt.After(time.Now())) {
|
||||
return base, nil
|
||||
}
|
||||
// The request-scoped forging resolver proves the material template against
|
||||
// the active PVF below. Do not require the durable inventory row to already
|
||||
// carry item_kind/pvf_path metadata: older, mailed and package-granted
|
||||
// one-time forging devices can be valid stackables while retaining a legacy
|
||||
// minimal row. Requiring pvfStackContract here rejected those tools before
|
||||
// their authoritative [portable genuine damage upgrade upgrede] document
|
||||
// was ever consulted.
|
||||
resolution, err := resolver(material.ItemID, int64(cmd.TargetItemTemplateID), oldLevel)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
switch {
|
||||
case resolution.TicketRandom:
|
||||
// [enchant random] + [action type] `[random upgrade separate]` family
|
||||
// (e.g. +6锻造券): the er_* target gates and the weighted candidate
|
||||
// roll are shared with the op50 ticket scene, applied here to the
|
||||
// forging level instead of the reinforcement level.
|
||||
if !validForgingRandomTicketResolution(resolution) {
|
||||
return base, nil
|
||||
}
|
||||
if !enchantRandomRestrictionAllows(resolution.RandomTicket, resolution.TargetEquipmentType, resolution.TargetRarity, target) {
|
||||
return base, nil
|
||||
}
|
||||
entry, ok := o.selectEnchantRandomEntry(resolution.RandomTicket.Entries)
|
||||
if !ok || entry.TargetLevel <= 0 || entry.TargetLevel > int64(resolution.MaxLevel) {
|
||||
return base, nil
|
||||
}
|
||||
resolution.TargetLevel = byte(entry.TargetLevel)
|
||||
resolution.SuccessWeight = forgingWeightFromEnchantRandom(entry.Weight)
|
||||
resolution.IndependentAttack = resolution.RandomIndependentAttacks[entry.TargetLevel]
|
||||
case strings.TrimSpace(resolution.TicketMode) == "":
|
||||
if !validForgingResolution(resolution, oldLevel) || !forgingDurabilityOK(target, resolution.TargetDurability) {
|
||||
return base, nil
|
||||
}
|
||||
default:
|
||||
if !validForgingTicketResolution(resolution, oldLevel) {
|
||||
return base, nil
|
||||
}
|
||||
}
|
||||
if material.ItemID != resolution.MaterialItemID || material.Count < resolution.MaterialCount {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
upgradeSucceeded := rand.Intn(currentForgingSuccessBase) < resolution.SuccessWeight
|
||||
newLevel := oldLevel
|
||||
if upgradeSucceeded {
|
||||
newLevel = resolution.TargetLevel
|
||||
setForgingLevel(&target, newLevel)
|
||||
switch cmd.TargetListType {
|
||||
case listTypeMain:
|
||||
inventoryRecord.Slots[targetKey] = target
|
||||
case listTypeEquipment:
|
||||
equipmentEntry.RawEntry = append([]byte(nil), target.RawEntry...)
|
||||
equipmentEntry.Extra = cloneStringMapForForging(target.Extra)
|
||||
equipmentRecord.Entries[targetKey] = equipmentEntry
|
||||
equipmentRecord.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveEquipmentFields(ctx, o.equipment, equipmentRecord, dnfrepo.EquipmentFieldEntries); err != nil {
|
||||
return base, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
material = cloneStack(material)
|
||||
material.Count -= resolution.MaterialCount
|
||||
if material.Count <= 0 {
|
||||
material = dnfrepo.ItemStack{}
|
||||
delete(inventoryRecord.Slots, materialKey)
|
||||
} else {
|
||||
updateStackRawAmount(&material)
|
||||
inventoryRecord.Slots[materialKey] = material
|
||||
}
|
||||
inventoryRecord.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, o.repo, inventoryRecord, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return base, err
|
||||
}
|
||||
|
||||
if cmd.TargetListType == listTypeEquipment {
|
||||
if upgradeSucceeded {
|
||||
target = forgingStackFromEquipmentEntry(equipmentRecord.Entries[targetKey])
|
||||
} else {
|
||||
target = forgingStackFromEquipmentEntry(equipmentEntry)
|
||||
}
|
||||
}
|
||||
base.Success = true
|
||||
base.UpgradeSucceeded = upgradeSucceeded
|
||||
base.MaterialItemID = resolution.MaterialItemID
|
||||
base.MaterialCost = resolution.MaterialCount
|
||||
base.MaterialRemainingCount = material.Count
|
||||
base.NewLevel = newLevel
|
||||
if upgradeSucceeded {
|
||||
base.IndependentAttack = resolution.IndependentAttack
|
||||
}
|
||||
base.SuccessWeight = resolution.SuccessWeight
|
||||
base.TargetUpdatedStack = target
|
||||
base.MaterialUpdatedStack = material
|
||||
base.Changed = true
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func validForgingResolution(resolution alignedcmd.ForgingResolution, oldLevel byte) bool {
|
||||
if !strings.EqualFold(strings.TrimSpace(resolution.TargetKind), "equipment") {
|
||||
return false
|
||||
}
|
||||
equipmentType := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(resolution.TargetEquipmentType, "`", "")))
|
||||
if equipmentType != "[weapon]" && equipmentType != "weapon" {
|
||||
return false
|
||||
}
|
||||
if resolution.MaxLevel == 0 || oldLevel >= resolution.MaxLevel || resolution.TargetLevel != oldLevel+1 || resolution.TargetLevel > resolution.MaxLevel {
|
||||
return false
|
||||
}
|
||||
return resolution.IndependentAttack >= 0 && resolution.SuccessWeight >= 0 && resolution.SuccessWeight <= currentForgingSuccessBase && resolution.MaterialItemID > 0 && resolution.MaterialCount > 0
|
||||
}
|
||||
|
||||
func validForgingTicketResolution(resolution alignedcmd.ForgingResolution, oldLevel byte) bool {
|
||||
if !strings.EqualFold(strings.TrimSpace(resolution.TargetKind), "equipment") {
|
||||
return false
|
||||
}
|
||||
equipmentType := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(resolution.TargetEquipmentType, "`", "")))
|
||||
if equipmentType != "[weapon]" && equipmentType != "weapon" {
|
||||
return false
|
||||
}
|
||||
mode := strings.ToLower(strings.TrimSpace(resolution.TicketMode))
|
||||
if mode != "fixed" && mode != "additional" {
|
||||
return false
|
||||
}
|
||||
if resolution.MaxLevel == 0 || resolution.TargetLevel == 0 || resolution.TargetLevel > resolution.MaxLevel {
|
||||
return false
|
||||
}
|
||||
if mode == "additional" && resolution.TargetLevel <= oldLevel {
|
||||
return false
|
||||
}
|
||||
return resolution.IndependentAttack >= 0 &&
|
||||
resolution.SuccessWeight >= 0 && resolution.SuccessWeight <= currentForgingSuccessBase &&
|
||||
resolution.MaterialItemID > 0 && resolution.MaterialCount == 1
|
||||
}
|
||||
|
||||
// validForgingRandomTicketResolution accepts the [enchant random] forging
|
||||
// coupon family before the candidate roll. The concrete level is not known
|
||||
// yet, so only the target class, the table bound and the one-coupon material
|
||||
// contract can be checked here.
|
||||
func validForgingRandomTicketResolution(resolution alignedcmd.ForgingResolution) bool {
|
||||
if !strings.EqualFold(strings.TrimSpace(resolution.TargetKind), "equipment") {
|
||||
return false
|
||||
}
|
||||
equipmentType := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(resolution.TargetEquipmentType, "`", "")))
|
||||
if equipmentType != "[weapon]" && equipmentType != "weapon" {
|
||||
return false
|
||||
}
|
||||
return resolution.MaxLevel > 0 && len(resolution.RandomTicket.Entries) > 0 &&
|
||||
resolution.MaterialItemID > 0 && resolution.MaterialCount == 1
|
||||
}
|
||||
|
||||
// forgingWeightFromEnchantRandom rescales an er_enchant weight (out of 100000)
|
||||
// to the native op438 probability base of 10000.
|
||||
func forgingWeightFromEnchantRandom(weight int64) int {
|
||||
scaled := weight / (enchantRandomWeightBase / currentForgingSuccessBase)
|
||||
if scaled < 0 {
|
||||
return 0
|
||||
}
|
||||
if scaled > currentForgingSuccessBase {
|
||||
return currentForgingSuccessBase
|
||||
}
|
||||
return int(scaled)
|
||||
}
|
||||
|
||||
func forgingDurabilityOK(stack dnfrepo.ItemStack, expected uint16) bool {
|
||||
if value := firstExtraInt(stack.Extra, -1, "durability"); value >= 0 {
|
||||
return value == int64(expected)
|
||||
}
|
||||
if len(stack.RawEntry) == currentItemListEntrySize {
|
||||
return binary.LittleEndian.Uint16(stack.RawEntry[0x0B:0x0D]) == expected
|
||||
}
|
||||
return expected == 0
|
||||
}
|
||||
|
||||
func forgingLevelOf(stack dnfrepo.ItemStack) byte {
|
||||
if raw := strings.TrimSpace(stack.Extra["forging_level"]); raw != "" {
|
||||
if value, err := strconv.ParseUint(raw, 0, 8); err == nil {
|
||||
return byte(value)
|
||||
}
|
||||
}
|
||||
if len(stack.RawEntry) == currentItemListEntrySize {
|
||||
return stack.RawEntry[currentForgingRawOffset]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func setForgingLevel(stack *dnfrepo.ItemStack, level byte) {
|
||||
if stack == nil {
|
||||
return
|
||||
}
|
||||
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
|
||||
stack.Extra = cloneStringMapForForging(stack.Extra)
|
||||
stack.Extra["forging_level"] = strconv.Itoa(int(level))
|
||||
if len(stack.RawEntry) == currentItemListEntrySize {
|
||||
stack.RawEntry[currentForgingRawOffset] = level
|
||||
// buildCommonItemListUpdateBody owns these opaque vectors through
|
||||
// Extra. Snapshot the current raw bytes so an op14 forging refresh
|
||||
// cannot clear adjacent current-client state.
|
||||
stack.Extra["raw_data_65"] = fmt.Sprintf("%x", stack.RawEntry[0x65:0x6E])
|
||||
if _, ok := stack.Extra["tail_data_72"]; !ok {
|
||||
stack.Extra["tail_data_72"] = fmt.Sprintf("%x", stack.RawEntry[0x72:0x77])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func forgingStackFromEquipmentEntry(entry dnfrepo.EquipmentEntry) dnfrepo.ItemStack {
|
||||
raw := append([]byte(nil), entry.RawEntry...)
|
||||
extra := cloneStringMapForForging(entry.Extra)
|
||||
if len(raw) == currentItemListEntrySize {
|
||||
setForgingProjectionFallback(extra, "amount_or_count", uint64(binary.LittleEndian.Uint32(raw[0x06:0x0A])))
|
||||
setForgingProjectionFallback(extra, "ext_data0", uint64(raw[0x0A]))
|
||||
setForgingProjectionFallback(extra, "durability", uint64(binary.LittleEndian.Uint16(raw[0x0B:0x0D])))
|
||||
setForgingProjectionFallback(extra, "seal_flag", uint64(raw[0x0D]))
|
||||
setForgingProjectionFallback(extra, "value_a", uint64(binary.LittleEndian.Uint32(raw[0x0E:0x12])))
|
||||
setForgingProjectionFallback(extra, "byte_12", uint64(raw[0x12]))
|
||||
setForgingProjectionFallback(extra, "byte_13", uint64(raw[0x13]))
|
||||
setForgingProjectionFallback(extra, "marker_16", uint64(binary.LittleEndian.Uint16(raw[0x14:0x16])))
|
||||
setForgingProjectionFallback(extra, "byte_57", uint64(raw[0x57]))
|
||||
setForgingProjectionFallback(extra, "byte_58", uint64(raw[0x58]))
|
||||
setForgingProjectionFallback(extra, "byte_59", uint64(raw[0x59]))
|
||||
if _, ok := extra["raw_data_65"]; !ok {
|
||||
extra["raw_data_65"] = fmt.Sprintf("%x", raw[0x65:0x6E])
|
||||
}
|
||||
if _, ok := extra["tail_data_72"]; !ok {
|
||||
extra["tail_data_72"] = fmt.Sprintf("%x", raw[0x72:0x77])
|
||||
}
|
||||
}
|
||||
return dnfrepo.ItemStack{
|
||||
ItemID: entry.ItemID,
|
||||
Count: 1,
|
||||
Bind: entry.Bind,
|
||||
ExpireAt: entry.ExpireAt,
|
||||
RawEntry: raw,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func setForgingProjectionFallback(extra map[string]string, key string, value uint64) {
|
||||
if _, ok := extra[key]; !ok {
|
||||
extra[key] = strconv.FormatUint(value, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneStringMapForForging(source map[string]string) map[string]string {
|
||||
out := make(map[string]string, len(source)+1)
|
||||
for key, value := range source {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user