Files
90CNServer/server/internal/modules/dnf/mail/body_cell_amount_test.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

152 lines
5.0 KiB
Go

package mail
// The amount a mailbox cell prints comes from the op0x61 summary's gold slot,
// not from the item row's count. The current reader takes it there and only
// overrides it for the gold certificates it can price from the item id:
//
// FUN_034e3a50(&gold); ; the summary's amount slot
// FUN_034e3a50(&itemID);
// if (itemID - 0x28eb7dU < 9) gold = (itemID - 0x28eb7c) * 1000000;
// else if (itemID - 0x28eb86U < 3) gold = (itemID - 0x28eb85) * 10000000;
// FUN_0270bfb0(0, claim, .., gold, ..);
//
// so a 代币券 attachment - money, credited to the account wallet on claim - must
// carry its count in that slot, or the seller's sale letter shows an icon and a
// zero. This is what the live consignment letter did.
import (
"encoding/binary"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// mailboxSummaryWireFields is one decoded summary row of the op0x61 page.
type mailboxSummaryWireFields struct {
claimObject uint32
amount uint32
itemID uint32
itemCount uint32
mailID uint32
}
func parseMailboxSummaryWire(t *testing.T, body []byte) []mailboxSummaryWireFields {
t.Helper()
if len(body) < 2 {
t.Fatalf("mailbox page too short: %d", len(body))
}
count := int(body[0])
off := 2 // summary count + clear-first flag
out := make([]mailboxSummaryWireFields, 0, count)
readU32 := func() uint32 {
if off+4 > len(body) {
t.Fatalf("mailbox page ran out at %d", off)
}
v := binary.LittleEndian.Uint32(body[off : off+4])
off += 4
return v
}
for index := 0; index < count; index++ {
var fields mailboxSummaryWireFields
fields.claimObject = readU32()
readU32() // sender wire id
nameLen := int(readU32())
off += nameLen
fields.amount = readU32()
fields.itemID = readU32()
off++ // has-item byte
fields.itemCount = readU32()
off += 2 + 1 + 4 + 1 + 1 + 2 // use count, bind, dword, two bytes, u16
off += mailboxItemRawSize
off += 4 // remaining seconds
fields.mailID = readU32()
off++ // mail type
out = append(out, fields)
}
return out
}
func TestMailboxPagePricesWalletVouchersInTheCellAmountSlot(t *testing.T) {
now := time.Unix(1_700_000_000, 0).UTC()
mail := dnfrepo.MailRecord{
MailID: "21",
SenderName: "系统",
Title: "- 已成功拍卖[1百万] -",
Body: "成交价 : + 188 代币券",
CreatedAt: now,
Attachments: []dnfrepo.MailAttachment{{ItemID: 2681762, Count: 132}},
}
body, err := buildMailboxListNotification([]dnfrepo.MailRecord{mail}, 0, now)
if err != nil {
t.Fatal(err)
}
summaries := parseMailboxSummaryWire(t, body)
if len(summaries) != 1 {
t.Fatalf("summaries=%d want 1", len(summaries))
}
got := summaries[0]
if got.itemID != 2681762 || got.itemCount != 132 {
t.Fatalf("voucher row item=%d count=%d want 2681762/132", got.itemID, got.itemCount)
}
if got.amount != 132 {
t.Fatalf("voucher cell amount=%d want the 代币券 count 132; the client prints this slot, not the item count", got.amount)
}
// The happy-token voucher is money too.
happy := mail
happy.MailID = "22"
happy.Attachments = []dnfrepo.MailAttachment{{ItemID: 2681917, Count: 7}}
body, err = buildMailboxListNotification([]dnfrepo.MailRecord{happy}, 0, now)
if err != nil {
t.Fatal(err)
}
if got := parseMailboxSummaryWire(t, body)[0]; got.amount != 7 {
t.Fatalf("happy voucher cell amount=%d want 7", got.amount)
}
// An ordinary item is not money: its cell keeps the zero amount and the
// client draws the item itself, so the count must not leak into this slot.
ordinary := mail
ordinary.MailID = "23"
ordinary.Attachments = []dnfrepo.MailAttachment{{ItemID: 28309, Count: 48}}
body, err = buildMailboxListNotification([]dnfrepo.MailRecord{ordinary}, 0, now)
if err != nil {
t.Fatal(err)
}
if got := parseMailboxSummaryWire(t, body)[0]; got.amount != 0 {
t.Fatalf("ordinary item cell amount=%d want 0", got.amount)
}
}
// A letter that carries gold as well keeps the gold on the row that owns it; the
// voucher row prices itself, and a single row cannot be both.
func TestMailboxPageKeepsLetterGoldAndVoucherAmountApartOnTheSameLetter(t *testing.T) {
now := time.Unix(1_700_000_000, 0).UTC()
mail := dnfrepo.MailRecord{
MailID: "31",
SenderName: "系统",
Title: "混合信件",
Body: "测试",
Gold: 19_500,
CreatedAt: now,
Attachments: []dnfrepo.MailAttachment{{ItemID: 2681762, Count: 132}},
}
body, err := buildMailboxListNotification([]dnfrepo.MailRecord{mail}, 0, now)
if err != nil {
t.Fatal(err)
}
summaries := parseMailboxSummaryWire(t, body)
if len(summaries) != 1 {
t.Fatalf("summaries=%d want 1", len(summaries))
}
// The first (and here only) row owns the letter's gold, so that is the amount
// it must print; the voucher count stays on the item row beside it.
if summaries[0].amount != 19_500 {
t.Fatalf("mixed letter cell amount=%d want the letter gold 19500", summaries[0].amount)
}
if summaries[0].itemCount != 132 {
t.Fatalf("mixed letter item count=%d want 132", summaries[0].itemCount)
}
}