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,235 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
)
|
||||
|
||||
func (s *Service) sendCurrentCeraShopPurchaseFailure(session *gameSession) error {
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem),
|
||||
buildCurrentCeraShopPurchaseFailureBody(),
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
// 90CN 商城**购买失败**回执码表:客户端按 `269 - code` 查这张表(**反序**)。
|
||||
//
|
||||
// ★ 这条规律是两次真机试码试出来的,不是抄来的:
|
||||
//
|
||||
// 2026-09-27 17:55 服务端回 code=11 → 客户端弹「错误 : 点券不足」= 字符串 258
|
||||
// 2026-09-27 18:1x 服务端回 code=12 → 客户端弹「错误 : 胜点不足」= 字符串 257
|
||||
//
|
||||
// 码 +1、字符串 −1 ⇒ `字符串id = 269 - code`(等价地 `code = 269 - 字符串id`)。
|
||||
// 我先前猜的 `247 + code` 在 code 12 上应该给出 259「Lv不足」,被第二次实测否掉。
|
||||
//
|
||||
// 客户端能显示的整张表(字符串 253..265,把 269 减过去就是码):
|
||||
//
|
||||
// id 253 无法购买的物品 → code 16
|
||||
// id 254 错误的商品编号 → code 15
|
||||
// id 255 物品栏空间不足 → code 14
|
||||
// id 256 金币不足 → code 13
|
||||
// id 257 胜点不足 → code 12 ← 实测确认
|
||||
// id 258 点券不足 → code 11 ← 实测确认
|
||||
// id 259 Lv不足 → code 10
|
||||
// id 260 不存在的物品 → code 9
|
||||
// id 261 使用期限设置错误 → code 8
|
||||
// id 262 物品功能设置错误 → code 7
|
||||
// id 263 付费服务器错误 → code 6
|
||||
// id 264 请求购买信息错误 → code 5
|
||||
// id 265 未知错误 → code 4
|
||||
//
|
||||
// ⚠⚠ 另一张表 `21969 + code`(21979 该商品无法赠送 / 21980 超出赠送金额上限 /
|
||||
// 21981 年龄不符合赠送条件 …)**op64 的回包够不到**:全客户端只有一处引用 21972
|
||||
// (RVA 0x00B0494D),而且在一个 `arg2 == 13` 的充值/商城页对话框里,不是按 code
|
||||
// 索引的购买回执表。用它的码去回,客户端会查到上面这张表里的**另一句话**。
|
||||
//
|
||||
// 后果:op64 阶段**表达不了赠送专用文案**(没有"赠送金额上限""年龄不符合赠送条件"
|
||||
// 这种句子)。所以约定是:**精确文案一律放在 op136 那一步**(那张表有
|
||||
// 70107/70108/70109 这些专用句子,好友 7 天、每日 10 万额度都在那一步回),
|
||||
// op64 只回这张表里语义最接近的一条,真实原因看服务端日志的 failure_code + reason。
|
||||
const (
|
||||
// currentCeraShopPurchaseFailureCodeUnknown 4 = 未知错误(畸形包、内部错误)。
|
||||
currentCeraShopPurchaseFailureCodeUnknown byte = 4
|
||||
// currentCeraShopPurchaseFailureCodeConnection 5 = 请求购买信息错误(瞬时故障)。
|
||||
currentCeraShopPurchaseFailureCodeConnection byte = 5
|
||||
// currentCeraShopPurchaseFailureCodeWrongCommodity 15 = 错误的商品编号。
|
||||
currentCeraShopPurchaseFailureCodeWrongCommodity byte = 15
|
||||
// currentCeraShopPurchaseFailureCodeInventoryFull 14 = 物品栏空间不足。
|
||||
currentCeraShopPurchaseFailureCodeInventoryFull byte = 14
|
||||
// currentCeraShopPurchaseFailureCodeInsufficientGold 13 = 金币不足。
|
||||
currentCeraShopPurchaseFailureCodeInsufficientGold byte = 13
|
||||
// currentCeraShopPurchaseFailureCodeInsufficientCera 11 = 点券不足(实测确认)。
|
||||
currentCeraShopPurchaseFailureCodeInsufficientCera byte = 11
|
||||
// currentCeraShopPurchaseFailureCodeLevelTooLow 10 = Lv不足。
|
||||
currentCeraShopPurchaseFailureCodeLevelTooLow byte = 10
|
||||
// currentCeraShopPurchaseFailureCodeCannotBuy 16 = 无法购买的物品。
|
||||
// 这是**兜底**码:语义最宽的"这一单买不成",任何没被单独映射的失败都用它。
|
||||
currentCeraShopPurchaseFailureCodeCannotBuy byte = 16
|
||||
)
|
||||
|
||||
// currentCeraShopPurchaseLimitFailureCode 是「商城限购已满」回给客户端的失败码。
|
||||
//
|
||||
// 限购在这张表里没有专用文案,用兜底码(无法购买的物品):客户端会弹一个明确提示
|
||||
// 而不是静默。真实原因在 `game-cera-shop-purchase-limit-rejected` 日志里可查。
|
||||
const currentCeraShopPurchaseLimitFailureCode byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
|
||||
// currentCeraShopPurchaseFailureCodeGiftUnsupported 是「这件商品不能赠送」的回执码。
|
||||
//
|
||||
// 这张表里没有"该商品无法赠送"这句话(那句在另一张表、op64 够不到),只能用兜底码。
|
||||
const currentCeraShopPurchaseFailureCodeGiftUnsupported byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
|
||||
// 赠送规则在 op64 阶段的回执码:
|
||||
//
|
||||
// 除"每日额度用完"外 → 16(无法购买的物品)
|
||||
//
|
||||
// ⚠ **op64 阶段表达不了赠送专用的文案**:能查的那张表只有 253..265 这 13 句通用购买
|
||||
// 错误(见文件顶部),"该商品无法赠送""年龄不符合赠送条件"这些句子在另一张表里、
|
||||
// op64 的回包够不到。所以约定是:**精确文案一律在 op136 阶段给**(0xA4 好友不满 7 天、
|
||||
// 0xA5 不是好友、0xA3 每日额度用完,各有专用句子),op64 只回语义最接近的一条,
|
||||
// 真实原因看服务端日志(`failure_code` + `reason`)。
|
||||
const (
|
||||
// currentCeraShopPurchaseFailureCodeGiftNotGiftable 商品不可邮寄 / 收件人职业不符。
|
||||
currentCeraShopPurchaseFailureCodeGiftNotGiftable byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
// currentCeraShopPurchaseFailureCodeGiftAccountTooNew 赠送方账号不满 7 天。
|
||||
currentCeraShopPurchaseFailureCodeGiftAccountTooNew byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
// currentCeraShopPurchaseFailureCodeGiftForbidden 其余赠送判定失败(好友/大区/自身)。
|
||||
currentCeraShopPurchaseFailureCodeGiftForbidden byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
// currentCeraShopPurchaseFailureCodeGiftAmountCap 每日赠送额度用完。
|
||||
//
|
||||
// 这张表里没有"超出赠送金额上限"这句话(那句在另一张表、op64 够不到),
|
||||
// 所以用兜底码「无法购买的物品」。精确的"每天最多10万"提示在 op136 阶段给(0xA3)。
|
||||
currentCeraShopPurchaseFailureCodeGiftAmountCap byte = currentCeraShopPurchaseFailureCodeCannotBuy
|
||||
)
|
||||
|
||||
// sendCurrentCeraShopPurchaseFailureWithCode 用指定码回 op64 失败包。
|
||||
func (s *Service) sendCurrentCeraShopPurchaseFailureWithCode(session *gameSession, code byte) error {
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem),
|
||||
buildCurrentCeraShopPurchaseFailureBodyWithCode(code),
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
// sendCurrentCeraShopGiftFailure 用指定的失败码回 op64 失败包(赠送路径专用)。
|
||||
//
|
||||
// 支持用试码文件临时覆盖(见 cera_shop_gift_limit.go 的说明):真机定位"哪个码显示
|
||||
// 哪句话"时不必重新编译。每次发送都会把**实际发出的码**记进日志,方便和玩家看到的
|
||||
// 提示一一对应。
|
||||
func (s *Service) sendCurrentCeraShopGiftFailure(session *gameSession, code byte) error {
|
||||
sent := code
|
||||
if override, ok := currentCeraShopGiftFailureCodeProbe(); ok {
|
||||
sent = override
|
||||
}
|
||||
if s != nil {
|
||||
s.logGameEvent(session, "game-cera-shop-gift-failure-acked",
|
||||
"char_id", 0,
|
||||
"mapped_code", code,
|
||||
"sent_code", sent,
|
||||
"probe_override", sent != code,
|
||||
"body_hex", fmt.Sprintf("%x", buildCurrentCeraShopPurchaseFailureBodyWithCode(sent)))
|
||||
}
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem),
|
||||
buildCurrentCeraShopPurchaseFailureBodyWithCode(sent),
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
// sendCurrentCeraShopGiftUnsupportedFailure 在"赠送形态收到但本服还不能投递"时回失败。
|
||||
func (s *Service) sendCurrentCeraShopGiftUnsupportedFailure(session *gameSession) error {
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem),
|
||||
buildCurrentCeraShopPurchaseFailureBodyWithCode(currentCeraShopPurchaseFailureCodeGiftUnsupported),
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
// sendCurrentCeraShopPurchaseLimitFailure 在限购已满时回失败包。
|
||||
func (s *Service) sendCurrentCeraShopPurchaseLimitFailure(session *gameSession) error {
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem),
|
||||
buildCurrentCeraShopPurchaseFailureBodyWithCode(currentCeraShopPurchaseLimitFailureCode),
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
func buildCurrentCeraShopPurchaseSuccessBody(commodityID uint32, bonuses ...currentCeraShopBonusItem) []byte {
|
||||
return buildCurrentCeraShopPurchaseSuccessBodyWithCount(commodityID, 0, bonuses...)
|
||||
}
|
||||
|
||||
// buildCurrentCeraShopPurchaseSuccessBodyWithCount keeps the three trailing
|
||||
// u32 fields zero. 2026-07-25 live evidence: writing the purchased quantity
|
||||
// into the first trailing u32 hangs the result dialog (client left
|
||||
// unclickable), so those fields are not the row counts. Quantity display
|
||||
// remains an open investigation; the zero shape is the proven safe one.
|
||||
func buildCurrentCeraShopPurchaseSuccessBodyWithCount(commodityID uint32, count uint32, bonuses ...currentCeraShopBonusItem) []byte {
|
||||
_ = count
|
||||
var writer packetWriter
|
||||
writer.writeByte(1)
|
||||
writer.writeByte(0)
|
||||
writer.writeInt32(-1) // category=-1: current reader performs a safe full catalog lookup.
|
||||
writer.writeUint32(commodityID)
|
||||
writer.writeUint32(0)
|
||||
writer.writeUint32(0)
|
||||
writer.writeUint32(0)
|
||||
// Bonus display rows drive the dialog's right-side 商城购物奖励 panel
|
||||
// (one-plus-one IPG gifts, e.g. 充满爱慕的信 -> 泰迪礼盒 + 幸运魔锤).
|
||||
// Row layout u32 itemID + u32 count.
|
||||
writer.writeUint16(uint16(len(bonuses)))
|
||||
for _, bonus := range bonuses {
|
||||
writer.writeUint32(bonus.ItemID)
|
||||
writer.writeUint32(bonus.Count)
|
||||
}
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func buildCurrentCeraShopPurchaseFailureBody() []byte {
|
||||
return buildCurrentCeraShopPurchaseFailureBodyWithCode(currentCeraShopPurchaseFailureCodeCannotBuy)
|
||||
}
|
||||
|
||||
// buildCurrentCeraShopPurchaseFailureBodyWithCode 生成商城购买失败包。形状固定为
|
||||
// `u8 0 + u8 失败码 + 5×u32`(见 86 参考对 sub_CD9490 失败分支的逆向:字段太少会
|
||||
// 让客户端读到下一个包的垃圾当索引而崩)。
|
||||
func buildCurrentCeraShopPurchaseFailureBodyWithCode(errorCode byte) []byte {
|
||||
var writer packetWriter
|
||||
writer.writeByte(0)
|
||||
writer.writeByte(errorCode)
|
||||
writer.writeInt32(-1)
|
||||
writer.writeInt32(-1)
|
||||
writer.writeUint32(0)
|
||||
writer.writeUint32(0)
|
||||
writer.writeUint32(0)
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func buildCurrentCeraShopBalanceBody(cera int64) []byte {
|
||||
return buildCurrentCeraShopWalletBody(cera, 0, 0)
|
||||
}
|
||||
|
||||
func buildCurrentCeraShopWalletBody(cera, tokenCera, happyTokenCera int64) []byte {
|
||||
var writer packetWriter
|
||||
writer.writeByte(1)
|
||||
writer.writeInt32(clampCurrentCeraBalance(cera))
|
||||
writer.writeInt32(clampCurrentCeraBalance(tokenCera))
|
||||
writer.writeInt32(clampCurrentCeraBalance(happyTokenCera))
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func clampCurrentCeraBalance(value int64) int {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
return int(value)
|
||||
}
|
||||
Reference in new issue
Block a user