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,230 @@
|
||||
package premium
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInventoryActivationAccountRequired = errors.New("premium inventory activation requires an account")
|
||||
ErrInventoryActivationInventoryRequired = errors.New("premium inventory activation requires an inventory")
|
||||
ErrInventoryActivationContractInvalid = errors.New("premium inventory activation contract is invalid")
|
||||
ErrInventoryActivationDurationOverflow = errors.New("premium inventory activation duration overflows")
|
||||
)
|
||||
|
||||
// InventoryContract is one runtime-PVF premiumlist item mapping. The bridge
|
||||
// resolves these values from the active PVF; inventory metadata is never an
|
||||
// authority for either type or duration.
|
||||
type InventoryContract struct {
|
||||
ItemID int64
|
||||
PremiumType int64
|
||||
DurationSeconds int64
|
||||
}
|
||||
|
||||
// InventoryActivation describes the final account entitlement produced by
|
||||
// consuming one or more inventory contract stacks of the same premium type.
|
||||
type InventoryActivation struct {
|
||||
PremiumType int64
|
||||
Units int64
|
||||
DurationSeconds int64
|
||||
ExpireAt int64
|
||||
}
|
||||
|
||||
// InventoryActivationResult is the deterministic result of consuming every
|
||||
// PVF-proven contract item in the selected character's main inventory.
|
||||
type InventoryActivationResult struct {
|
||||
Activations []InventoryActivation
|
||||
RemovedSlots []int16
|
||||
}
|
||||
|
||||
func (r InventoryActivationResult) Changed() bool {
|
||||
return len(r.RemovedSlots) != 0 || len(r.Activations) != 0
|
||||
}
|
||||
|
||||
// ActivateContractUnits converts explicit contract units (item id -> count) into
|
||||
// account expiry time without requiring an inventory row.
|
||||
//
|
||||
// Reward flows that roll premium contract items together with ordinary rewards
|
||||
// use this so a contract never occupies a bag cell: the cell stays available for
|
||||
// a real reward instead of being filled and swept a moment later, which is what
|
||||
// pushed ordinary rewards into the overflow mail. The account entitlement math is
|
||||
// identical to ActivateInventoryContracts; only the source of the units differs.
|
||||
func ActivateContractUnits(
|
||||
account *dnfrepo.AccountRecord,
|
||||
contracts map[int64]InventoryContract,
|
||||
units map[int64]int64,
|
||||
now time.Time,
|
||||
) (InventoryActivationResult, error) {
|
||||
if account == nil || strings.TrimSpace(account.AccountID) == "" {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationAccountRequired
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
secondsByType := make(map[int64]int64)
|
||||
unitsByType := make(map[int64]int64)
|
||||
for itemID, count := range units {
|
||||
if count <= 0 {
|
||||
continue
|
||||
}
|
||||
contract, exists := contracts[itemID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if contract.ItemID != itemID || contract.PremiumType <= 0 || contract.DurationSeconds <= 0 {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationContractInvalid
|
||||
}
|
||||
if count > math.MaxInt64/contract.DurationSeconds {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
delta := count * contract.DurationSeconds
|
||||
if secondsByType[contract.PremiumType] > math.MaxInt64-delta ||
|
||||
unitsByType[contract.PremiumType] > math.MaxInt64-count {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
secondsByType[contract.PremiumType] += delta
|
||||
unitsByType[contract.PremiumType] += count
|
||||
}
|
||||
if len(secondsByType) == 0 {
|
||||
return InventoryActivationResult{}, nil
|
||||
}
|
||||
for premiumType, seconds := range secondsByType {
|
||||
base := now.Unix()
|
||||
if current := ExpireAt(*account, premiumType); current > base {
|
||||
base = current
|
||||
}
|
||||
if seconds > math.MaxInt64-base {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
}
|
||||
|
||||
types := make([]int64, 0, len(secondsByType))
|
||||
for premiumType := range secondsByType {
|
||||
types = append(types, premiumType)
|
||||
}
|
||||
sort.Slice(types, func(i, j int) bool { return types[i] < types[j] })
|
||||
result := InventoryActivationResult{}
|
||||
for _, premiumType := range types {
|
||||
Upsert(account, premiumType, secondsByType[premiumType], 1, now)
|
||||
result.Activations = append(result.Activations, InventoryActivation{
|
||||
PremiumType: premiumType,
|
||||
Units: unitsByType[premiumType],
|
||||
DurationSeconds: secondsByType[premiumType],
|
||||
ExpireAt: ExpireAt(*account, premiumType),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type inventoryActivationPlan struct {
|
||||
key string
|
||||
slot int16
|
||||
}
|
||||
|
||||
// ActivateInventoryContracts converts all PVF-proven contract stacks in list
|
||||
// 0 into account expiry time. Validation is completed before either aggregate
|
||||
// is mutated, so callers can safely use this inside their repository unit of
|
||||
// work without exposing a partial result.
|
||||
func ActivateInventoryContracts(
|
||||
account *dnfrepo.AccountRecord,
|
||||
inventory *dnfrepo.InventoryRecord,
|
||||
contracts map[int64]InventoryContract,
|
||||
now time.Time,
|
||||
) (InventoryActivationResult, error) {
|
||||
if account == nil || strings.TrimSpace(account.AccountID) == "" {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationAccountRequired
|
||||
}
|
||||
if inventory == nil || strings.TrimSpace(inventory.CharacterID) == "" {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationInventoryRequired
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
plans := make([]inventoryActivationPlan, 0)
|
||||
secondsByType := make(map[int64]int64)
|
||||
unitsByType := make(map[int64]int64)
|
||||
for key, stack := range inventory.Slots {
|
||||
slot, main := premiumMainInventorySlot(key)
|
||||
if !main || stack.ItemID <= 0 || stack.Count <= 0 {
|
||||
continue
|
||||
}
|
||||
contract, exists := contracts[stack.ItemID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if contract.ItemID != stack.ItemID || contract.PremiumType <= 0 || contract.DurationSeconds <= 0 {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationContractInvalid
|
||||
}
|
||||
if stack.Count > math.MaxInt64/contract.DurationSeconds {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
delta := stack.Count * contract.DurationSeconds
|
||||
if secondsByType[contract.PremiumType] > math.MaxInt64-delta ||
|
||||
unitsByType[contract.PremiumType] > math.MaxInt64-stack.Count {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
secondsByType[contract.PremiumType] += delta
|
||||
unitsByType[contract.PremiumType] += stack.Count
|
||||
plans = append(plans, inventoryActivationPlan{key: key, slot: slot})
|
||||
}
|
||||
if len(plans) == 0 {
|
||||
return InventoryActivationResult{}, nil
|
||||
}
|
||||
|
||||
for premiumType, seconds := range secondsByType {
|
||||
base := now.Unix()
|
||||
if current := ExpireAt(*account, premiumType); current > base {
|
||||
base = current
|
||||
}
|
||||
if seconds > math.MaxInt64-base {
|
||||
return InventoryActivationResult{}, ErrInventoryActivationDurationOverflow
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(plans, func(i, j int) bool {
|
||||
if plans[i].slot == plans[j].slot {
|
||||
return plans[i].key < plans[j].key
|
||||
}
|
||||
return plans[i].slot < plans[j].slot
|
||||
})
|
||||
result := InventoryActivationResult{RemovedSlots: make([]int16, 0, len(plans))}
|
||||
for _, plan := range plans {
|
||||
delete(inventory.Slots, plan.key)
|
||||
result.RemovedSlots = append(result.RemovedSlots, plan.slot)
|
||||
}
|
||||
types := make([]int64, 0, len(secondsByType))
|
||||
for premiumType := range secondsByType {
|
||||
types = append(types, premiumType)
|
||||
}
|
||||
sort.Slice(types, func(i, j int) bool { return types[i] < types[j] })
|
||||
for _, premiumType := range types {
|
||||
Upsert(account, premiumType, secondsByType[premiumType], 1, now)
|
||||
result.Activations = append(result.Activations, InventoryActivation{
|
||||
PremiumType: premiumType,
|
||||
Units: unitsByType[premiumType],
|
||||
DurationSeconds: secondsByType[premiumType],
|
||||
ExpireAt: ExpireAt(*account, premiumType),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func premiumMainInventorySlot(key string) (int16, bool) {
|
||||
if !strings.HasPrefix(key, "0:") {
|
||||
return 0, false
|
||||
}
|
||||
raw := strings.TrimSpace(strings.TrimPrefix(key, "0:"))
|
||||
value, err := strconv.ParseInt(raw, 10, 16)
|
||||
if err != nil || value < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return int16(value), true
|
||||
}
|
||||
Reference in new issue
Block a user