Files
90CNServer/server/internal/modules/dnf/cerashop/voucher.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

144 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package cerashop:代币券 / 欢乐代币券的"入钱包"语义。
//
// 背景(现场 2026-09-19):代币券和欢乐代币券在道具 PVF 里是普通
// `[stackable type] [material]`,既没有 `[action type]` 也没有任何"使用后转成点券"
// 的定义 —— 也就是说**客户端自己根本无法把它们变成钱包余额**。任何把它们当普通道具
// 发进背包的路径,玩家拿到的都是一件永远用不掉的"材料",这正是现场的抱怨。
//
// 所以转换 100% 由服务端负责:发放时识别券的道具号,直接累加到账号钱包,不占背包格。
//
// 道具号来源(真机 runtime/data/dnf/Script.pvf):
//
// stackable/cash/chn_Trade_Gold/trade_point.stk id 2681762 `代币券`
// stackable/cash/chn_Trade_Gold/trade_point_event.stk id 2681917 `欢乐代币券`
package cerashop
import (
"errors"
"math"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
// TokenCeraVoucherItemID 是 `代币券`(trade_point.stk)。
TokenCeraVoucherItemID int64 = 2681762
// HappyTokenCeraVoucherItemID 是 `欢乐代币券`(trade_point_event.stk)。
HappyTokenCeraVoucherItemID int64 = 2681917
)
// MaxWalletBalance 是三个钱包字段的上限。
//
// 客户端把这三个值按 **有符号 32 位** 读进字段(反编译 `class0/0x0035` 的 handler
// `FUN_01d83c20` 读的是 i32),所以上限必须是 MaxInt32 而不是 MaxInt64:
// 超过就会在客户端显示成负数。所有入账都必须在这里挡住。
const MaxWalletBalance int64 = math.MaxInt32
// ErrWalletOverflow 表示入账会让钱包超过 MaxWalletBalance。
// 调用方应该把它映射成自己模块的"请求非法"错误,而不是静默截断。
var ErrWalletOverflow = errors.New("cera shop: wallet balance would exceed the client limit")
// VoucherKind 是券对应的钱包字段。
type VoucherKind int
const (
// VoucherNone 表示这件道具不是券。
VoucherNone VoucherKind = iota
// VoucherTokenCera 是代币券,入账到 Wallet.TokenCera。
VoucherTokenCera
// VoucherHappyTokenCera 是欢乐代币券,入账到 Wallet.HappyTokenCera。
VoucherHappyTokenCera
)
// VoucherKindOf 判断一件道具是不是券。不是券返回 VoucherNone。
func VoucherKindOf(itemID int64) VoucherKind {
switch itemID {
case TokenCeraVoucherItemID:
return VoucherTokenCera
case HappyTokenCeraVoucherItemID:
return VoucherHappyTokenCera
default:
return VoucherNone
}
}
// IsVoucher 是上面的布尔简写,给"这批奖励里有没有券"这类判断用。
func IsVoucher(itemID int64) bool {
return VoucherKindOf(itemID) != VoucherNone
}
// VoucherItemID 返回某类券的道具号(VoucherNone 返回 0)。
func VoucherItemID(kind VoucherKind) int64 {
switch kind {
case VoucherTokenCera:
return TokenCeraVoucherItemID
case VoucherHappyTokenCera:
return HappyTokenCeraVoucherItemID
default:
return 0
}
}
// VoucherItemIDs 返回全部券道具号,供"发放前先摘出来"的过滤使用。
func VoucherItemIDs() []int64 {
return []int64{TokenCeraVoucherItemID, HappyTokenCeraVoucherItemID}
}
// VoucherCounts 是一批待入账的券数量。
type VoucherCounts struct {
TokenCera int64
HappyTokenCera int64
}
// IsZero 表示没有任何券需要入账。
func (c VoucherCounts) IsZero() bool {
return c.TokenCera == 0 && c.HappyTokenCera == 0
}
// Add 累加另一种券的数量。
func (c VoucherCounts) Add(other VoucherCounts) VoucherCounts {
return VoucherCounts{
TokenCera: c.TokenCera + other.TokenCera,
HappyTokenCera: c.HappyTokenCera + other.HappyTokenCera,
}
}
// CreditVoucherCounts 把券数量累加到账号钱包。
//
// 语义与既有邮件领取路径完全一致(那里已经这么做了),本函数只是把它提取成共享实现,
// 让任务奖励、礼盒开启、GM 补发都走同一条路,避免各写一套溢出判断。
//
// - **全有或全无**:任何一项溢出都不写任何字段,账号记录保持原样,返回 ErrWalletOverflow。
// - 调用方负责保存 account 与它的 UpdatedAt。
// - 负数一律当 0:券数量为负只可能来自上游 bug,不该把余额扣下去。
func CreditVoucherCounts(account *dnfrepo.AccountRecord, counts VoucherCounts) (Wallet, error) {
if account == nil {
return Wallet{}, errors.New("cera shop: account is required")
}
tokenCera := counts.TokenCera
if tokenCera < 0 {
tokenCera = 0
}
happyTokenCera := counts.HappyTokenCera
if happyTokenCera < 0 {
happyTokenCera = 0
}
if tokenCera == 0 && happyTokenCera == 0 {
return WalletBalance(*account), nil
}
wallet := WalletBalance(*account)
if tokenCera > MaxWalletBalance-wallet.TokenCera {
return wallet, ErrWalletOverflow
}
if happyTokenCera > MaxWalletBalance-wallet.HappyTokenCera {
return wallet, ErrWalletOverflow
}
if tokenCera > 0 {
SetTokenCeraBalance(account, wallet.TokenCera+tokenCera)
}
if happyTokenCera > 0 {
SetHappyTokenCeraBalance(account, wallet.HappyTokenCera+happyTokenCera)
}
return WalletBalance(*account), nil
}