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,353 @@
|
||||
package itemgrade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
type ResealRangeWeight struct {
|
||||
Minimum int64
|
||||
Maximum int64
|
||||
Weight int64
|
||||
}
|
||||
|
||||
type ResealConfig struct {
|
||||
GradeWeights []ResealRangeWeight
|
||||
RarityWeights map[int64]float64
|
||||
ExtendedWeights map[int64]float64
|
||||
SoulItemIDs map[int64]uint32
|
||||
SoulCostWeights map[int64]float64
|
||||
ResealConstants map[int64]float64
|
||||
ResealCountLimits []ResealRangeWeight
|
||||
}
|
||||
|
||||
type ResealItemDefinition struct {
|
||||
Equipment bool
|
||||
MinimumLevel int64
|
||||
Rarity int64
|
||||
Wax bool
|
||||
WaxOneShot bool
|
||||
ResealLevelMin int64
|
||||
ResealLevelMax int64
|
||||
}
|
||||
|
||||
type ResealCatalog interface {
|
||||
ResolveResealItem(uint32) (ResealItemDefinition, error)
|
||||
}
|
||||
|
||||
type ResealCommand struct {
|
||||
AccountID string
|
||||
CharacterID string
|
||||
TargetSlot int16
|
||||
TargetItemID uint32
|
||||
MaterialSlot int16
|
||||
Mode uint16
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ResealResult struct {
|
||||
TargetSlot int16
|
||||
TargetStack dnfrepo.ItemStack
|
||||
MaterialSlot int16
|
||||
MaterialRemaining int64
|
||||
AccountMaterial bool
|
||||
ResealCount byte
|
||||
}
|
||||
|
||||
func (o *Owner) Reseal(ctx context.Context, catalog ResealCatalog, config ResealConfig, cmd ResealCommand) (ResealResult, error) {
|
||||
if o == nil || catalog == nil || o.characterItems == nil || o.accountItems == nil {
|
||||
return ResealResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
accountID := strings.TrimSpace(cmd.AccountID)
|
||||
characterID := strings.TrimSpace(cmd.CharacterID)
|
||||
if characterID == "" || cmd.TargetItemID == 0 {
|
||||
return ResealResult{}, ErrCharacterRequired
|
||||
}
|
||||
definition, err := catalog.ResolveResealItem(cmd.TargetItemID)
|
||||
if err != nil || !definition.Equipment {
|
||||
return ResealResult{}, fmt.Errorf("%w: reseal target definition", ErrTargetMissing)
|
||||
}
|
||||
now := cmd.UpdatedAt
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
} else {
|
||||
now = now.UTC()
|
||||
}
|
||||
var committed ResealResult
|
||||
switch cmd.Mode {
|
||||
case 1:
|
||||
if accountID == "" {
|
||||
return ResealResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
err = o.accountItems.WithinAccountCharacterItems(ctx, accountID, characterID, func(accounts dnfrepo.AccountInventoryRepository, inventories dnfrepo.InventoryRepository) error {
|
||||
inventory, found, loadErr := inventories.Load(ctx, characterID)
|
||||
if loadErr != nil || !found {
|
||||
return resealFallbackError(loadErr, errors.New("reseal inventory missing"))
|
||||
}
|
||||
account, found, loadErr := accounts.Load(ctx, accountID)
|
||||
if loadErr != nil || !found {
|
||||
return resealFallbackError(loadErr, errors.New("reseal account inventory missing"))
|
||||
}
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
account = dnfrepo.CloneAccountInventory(account)
|
||||
target, oldCount, mutationErr := prepareResealTarget(inventory, cmd, definition, config, false)
|
||||
if mutationErr != nil {
|
||||
return mutationErr
|
||||
}
|
||||
materialID, cost, supported := config.materialAndCost(definition, oldCount)
|
||||
if !supported {
|
||||
return errors.New("reseal material rule unavailable")
|
||||
}
|
||||
materialSlot, remaining, consumeErr := consumeResealItemByID(account.Slots, materialID, cost)
|
||||
if consumeErr != nil {
|
||||
return consumeErr
|
||||
}
|
||||
inventory.Slots[resealInventoryKey(cmd.TargetSlot)] = target
|
||||
inventory.UpdatedAt = now
|
||||
account.UpdatedAt = now
|
||||
if saveErr := dnfrepo.SaveInventoryFields(ctx, inventories, inventory, dnfrepo.InventoryFieldSlots); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
if saveErr := accounts.Save(ctx, account); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
committed = ResealResult{TargetSlot: cmd.TargetSlot, TargetStack: target, MaterialSlot: materialSlot, MaterialRemaining: remaining, AccountMaterial: true, ResealCount: ResealCount(target)}
|
||||
return nil
|
||||
})
|
||||
case 0:
|
||||
err = o.characterItems.WithinCharacterItems(ctx, characterID, func(inventories dnfrepo.InventoryRepository, _ dnfrepo.EquipmentRepository) error {
|
||||
inventory, found, loadErr := inventories.Load(ctx, characterID)
|
||||
if loadErr != nil || !found {
|
||||
return resealFallbackError(loadErr, errors.New("reseal inventory missing"))
|
||||
}
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
materialKey := resealInventoryKey(cmd.MaterialSlot)
|
||||
material, found := inventory.Slots[materialKey]
|
||||
if !found || material.ItemID <= 0 || material.ItemID > math.MaxUint32 || material.Count <= 0 {
|
||||
return ErrMaterialMissing
|
||||
}
|
||||
wax, resolveErr := catalog.ResolveResealItem(uint32(material.ItemID))
|
||||
if resolveErr != nil || !wax.Wax {
|
||||
return ErrMaterialMissing
|
||||
}
|
||||
target, oldCount, mutationErr := prepareResealTarget(inventory, cmd, definition, config, wax.WaxOneShot)
|
||||
if mutationErr != nil {
|
||||
return mutationErr
|
||||
}
|
||||
if wax.ResealLevelMin > 0 && definition.MinimumLevel < wax.ResealLevelMin || wax.ResealLevelMax > 0 && definition.MinimumLevel > wax.ResealLevelMax {
|
||||
return errors.New("reseal wax level mismatch")
|
||||
}
|
||||
cost := int64(1)
|
||||
if !wax.WaxOneShot {
|
||||
var supported bool
|
||||
cost, supported = config.waxCost(definition, oldCount)
|
||||
if !supported {
|
||||
return errors.New("reseal wax cost unavailable")
|
||||
}
|
||||
}
|
||||
if material.Count < cost {
|
||||
return ErrMaterialMissing
|
||||
}
|
||||
material.Count -= cost
|
||||
if material.Count == 0 {
|
||||
delete(inventory.Slots, materialKey)
|
||||
} else {
|
||||
inventory.Slots[materialKey] = material
|
||||
}
|
||||
inventory.Slots[resealInventoryKey(cmd.TargetSlot)] = target
|
||||
inventory.UpdatedAt = now
|
||||
if saveErr := dnfrepo.SaveInventoryFields(ctx, inventories, inventory, dnfrepo.InventoryFieldSlots); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
committed = ResealResult{TargetSlot: cmd.TargetSlot, TargetStack: target, MaterialSlot: cmd.MaterialSlot, MaterialRemaining: material.Count, ResealCount: ResealCount(target)}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
return ResealResult{}, errors.New("unsupported reseal mode")
|
||||
}
|
||||
if err != nil {
|
||||
return ResealResult{}, err
|
||||
}
|
||||
return committed, nil
|
||||
}
|
||||
|
||||
func prepareResealTarget(inventory dnfrepo.InventoryRecord, cmd ResealCommand, definition ResealItemDefinition, config ResealConfig, oneShot bool) (dnfrepo.ItemStack, byte, error) {
|
||||
target, found := inventory.Slots[resealInventoryKey(cmd.TargetSlot)]
|
||||
if !found || target.ItemID != int64(cmd.TargetItemID) || target.ItemID <= 0 {
|
||||
return dnfrepo.ItemStack{}, 0, ErrTargetMissing
|
||||
}
|
||||
if SealFlag(target) != 0 {
|
||||
return dnfrepo.ItemStack{}, 0, errors.New("reseal target already sealed")
|
||||
}
|
||||
oldCount := ResealCount(target)
|
||||
limit := config.countLimit(definition.MinimumLevel)
|
||||
if !oneShot && (limit == 0 || oldCount >= limit) {
|
||||
return dnfrepo.ItemStack{}, oldCount, errors.New("reseal count limit reached")
|
||||
}
|
||||
newCount := oldCount
|
||||
if !oneShot {
|
||||
newCount++
|
||||
}
|
||||
return applyResealState(target, newCount), oldCount, nil
|
||||
}
|
||||
|
||||
func applyResealState(stack dnfrepo.ItemStack, count byte) dnfrepo.ItemStack {
|
||||
if stack.Extra == nil {
|
||||
stack.Extra = make(map[string]string)
|
||||
}
|
||||
packed := packedResealByte(stack)
|
||||
packed = (packed & 0x1f) | ((count & 0x07) << 5)
|
||||
stack.Extra["ext_data0"] = strconv.Itoa(int(packed))
|
||||
stack.Extra["packed_flag_byte"] = strconv.Itoa(int(packed))
|
||||
stack.Extra["reseal_count"] = strconv.Itoa(int(count))
|
||||
stack.Extra["seal_flag"] = "1"
|
||||
stack.Extra["seal"] = "1"
|
||||
stack.Bind = true
|
||||
if len(stack.RawEntry) > 0x0d {
|
||||
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
|
||||
stack.RawEntry[0x0a] = packed
|
||||
stack.RawEntry[0x0d] = 1
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
func SealFlag(stack dnfrepo.ItemStack) byte {
|
||||
if len(stack.RawEntry) > 0x0d {
|
||||
return stack.RawEntry[0x0d]
|
||||
}
|
||||
for _, key := range []string{"seal_flag", "seal", "sealed"} {
|
||||
if value, err := strconv.ParseUint(strings.TrimSpace(stack.Extra[key]), 10, 8); err == nil && value != 0 {
|
||||
return byte(value)
|
||||
}
|
||||
}
|
||||
if stack.Bind {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ResealCount(stack dnfrepo.ItemStack) byte {
|
||||
if value, err := strconv.ParseUint(strings.TrimSpace(stack.Extra["reseal_count"]), 10, 8); err == nil && value <= 7 {
|
||||
return byte(value)
|
||||
}
|
||||
return packedResealByte(stack) >> 5
|
||||
}
|
||||
|
||||
func packedResealByte(stack dnfrepo.ItemStack) byte {
|
||||
if len(stack.RawEntry) > 0x0a {
|
||||
return stack.RawEntry[0x0a]
|
||||
}
|
||||
value, _ := strconv.ParseUint(strings.TrimSpace(stack.Extra["packed_flag_byte"]), 10, 8)
|
||||
if value == 0 {
|
||||
value, _ = strconv.ParseUint(strings.TrimSpace(stack.Extra["ext_data0"]), 10, 8)
|
||||
}
|
||||
return byte(value)
|
||||
}
|
||||
|
||||
func (c ResealConfig) materialAndCost(definition ResealItemDefinition, count byte) (uint32, int64, bool) {
|
||||
if definition.Rarity < 0 || definition.Rarity >= 7 || definition.MinimumLevel <= 0 || count >= 7 {
|
||||
return 0, 0, false
|
||||
}
|
||||
grade := resealRangeWeight(c.GradeWeights, definition.MinimumLevel)
|
||||
weight := c.RarityWeights[definition.Rarity] * c.ExtendedWeights[definition.Rarity] * c.SoulCostWeights[definition.Rarity]
|
||||
constant := c.ResealConstants[definition.Rarity]
|
||||
cost := int64(float64(grade) * weight * math.Pow(constant, float64(count)))
|
||||
itemID := c.SoulItemIDs[definition.Rarity]
|
||||
return itemID, cost, grade > 0 && weight > 0 && constant > 0 && itemID > 0 && cost > 0
|
||||
}
|
||||
|
||||
func (c ResealConfig) waxCost(definition ResealItemDefinition, count byte) (int64, bool) {
|
||||
if definition.Rarity < 0 || definition.Rarity >= 7 || definition.MinimumLevel <= 0 || count >= 7 {
|
||||
return 0, false
|
||||
}
|
||||
grade := resealRangeWeight(c.GradeWeights, definition.MinimumLevel)
|
||||
weight := c.RarityWeights[definition.Rarity] * c.ExtendedWeights[definition.Rarity]
|
||||
constant := c.ResealConstants[definition.Rarity]
|
||||
cost := int64(float64(grade) * weight * math.Pow(constant, float64(count)))
|
||||
return cost, grade > 0 && weight > 0 && constant > 0 && cost > 0
|
||||
}
|
||||
|
||||
func (c ResealConfig) countLimit(level int64) byte {
|
||||
value := resealRangeWeight(c.ResealCountLimits, level)
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 7 {
|
||||
value = 7
|
||||
}
|
||||
return byte(value)
|
||||
}
|
||||
|
||||
func resealRangeWeight(rows []ResealRangeWeight, value int64) int64 {
|
||||
for _, row := range rows {
|
||||
if value >= row.Minimum && value <= row.Maximum {
|
||||
return row.Weight
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func consumeResealItemByID(slots map[string]dnfrepo.ItemStack, itemID uint32, count int64) (int16, int64, error) {
|
||||
if count <= 0 {
|
||||
return 0, 0, errors.New("invalid reseal material count")
|
||||
}
|
||||
keys := make([]string, 0)
|
||||
available := int64(0)
|
||||
for key, stack := range slots {
|
||||
if stack.ItemID == int64(itemID) && stack.Count > 0 {
|
||||
keys = append(keys, key)
|
||||
available += stack.Count
|
||||
}
|
||||
}
|
||||
if available < count {
|
||||
return 0, 0, ErrMaterialMissing
|
||||
}
|
||||
sort.Strings(keys)
|
||||
need := count
|
||||
var lastSlot int16
|
||||
var lastRemaining int64
|
||||
for _, key := range keys {
|
||||
stack := slots[key]
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) == 2 {
|
||||
if value, err := strconv.ParseInt(parts[1], 10, 16); err == nil {
|
||||
lastSlot = int16(value)
|
||||
}
|
||||
}
|
||||
used := stack.Count
|
||||
if used > need {
|
||||
used = need
|
||||
}
|
||||
stack.Count -= used
|
||||
need -= used
|
||||
lastRemaining = stack.Count
|
||||
if stack.Count == 0 {
|
||||
delete(slots, key)
|
||||
} else {
|
||||
slots[key] = stack
|
||||
}
|
||||
if need == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return lastSlot, lastRemaining, nil
|
||||
}
|
||||
|
||||
func resealInventoryKey(slot int16) string {
|
||||
return fmt.Sprintf("%d:%d", dnfrepo.MainInventoryListType, slot)
|
||||
}
|
||||
|
||||
func resealFallbackError(err, fallback error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in new issue
Block a user