Files
90CNServer/server/internal/services/dnfbridge/daily_delete_item_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

154 lines
6.4 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 dnfbridge
import (
"os"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/premium"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestStampCurrentDailyDeleteItemOnlyTouchesDailyDeleteItems(t *testing.T) {
ordinary := dnfrepo.ItemStack{ItemID: 3011}
if _, changed := stampCurrentDailyDeleteItem(ordinary, dungeonDropItemDefinition{}, 100); changed {
t.Fatal("a non daily-delete item must not be stamped")
}
stack, changed := stampCurrentDailyDeleteItem(
dnfrepo.ItemStack{ItemID: 490001382}, dungeonDropItemDefinition{DailyDeleteItem: true}, 100)
if !changed {
t.Fatal("a daily-delete item must be stamped on grant")
}
if got := stack.Extra[currentDailyDeleteItemStateKey]; got != "100" {
t.Fatalf("stamp = %q, want %q", got, "100")
}
// 重复盖同一天是幂等的,不该把行标记成脏的。
if _, again := stampCurrentDailyDeleteItem(stack, dungeonDropItemDefinition{DailyDeleteItem: true}, 100); again {
t.Fatal("re-stamping the same service day must be a no-op")
}
}
func TestCurrentDailyDeleteItemDue(t *testing.T) {
stamped := func(day string) dnfrepo.ItemStack {
return dnfrepo.ItemStack{ItemID: 490001382, Extra: map[string]string{currentDailyDeleteItemStateKey: day}}
}
for _, row := range []struct {
name string
stack dnfrepo.ItemStack
serviceDay int64
wantDue bool
wantStamp bool
}{
{name: "同一天不删", stack: stamped("100"), serviceDay: 100, wantDue: false, wantStamp: true},
{name: "跨过边界就删", stack: stamped("100"), serviceDay: 101, wantDue: true, wantStamp: true},
{name: "跨多天也删", stack: stamped("100"), serviceDay: 137, wantDue: true, wantStamp: true},
// 时钟回拨/存档搬迁不该把东西删掉。
{name: "戳在未来不删", stack: stamped("200"), serviceDay: 100, wantDue: false, wantStamp: true},
// 没有戳的是存量:补盖,不是没收。
{name: "没有戳", stack: dnfrepo.ItemStack{ItemID: 490001382}, serviceDay: 100, wantDue: false, wantStamp: false},
// 读不懂的戳同样按存量处理,宁可多留一天也别凭坏数据删东西。
{name: "戳坏了", stack: stamped("not-a-day"), serviceDay: 100, wantDue: false, wantStamp: false},
} {
t.Run(row.name, func(t *testing.T) {
due, hasStamp := currentDailyDeleteItemDue(row.stack, row.serviceDay)
if due != row.wantDue || hasStamp != row.wantStamp {
t.Fatalf("due=%t stamped=%t, want due=%t stamped=%t", due, hasStamp, row.wantDue, row.wantStamp)
}
})
}
}
// 边界必须是北京 06:00,和 premium 的服务日是同一个,否则每日删除会和签到、
// 疲劳重置错开一截。
func TestDailyDeleteUsesTheServiceDayBoundary(t *testing.T) {
// 2026-09-16 05:59 北京 = 前一个服务日;06:01 北京 = 新的服务日。
beijing := time.FixedZone("CST", 8*3600)
before := time.Date(2026, 9, 16, 5, 59, 0, 0, beijing)
after := time.Date(2026, 9, 16, 6, 1, 0, 0, beijing)
if premium.ServiceDayID(before.UTC()) == premium.ServiceDayID(after.UTC()) {
t.Fatal("06:00 Beijing must separate two service days")
}
// 同一天内的两个时刻必须落在同一个服务日。
noon := time.Date(2026, 9, 16, 12, 0, 0, 0, beijing)
if premium.ServiceDayID(after.UTC()) != premium.ServiceDayID(noon.UTC()) {
t.Fatal("06:01 and 12:00 on the same day must share a service day")
}
}
// 发放路径统一在 applyCurrentPVFItemExpirationAt 里盖戳:本服屏蔽了物品期限,
// 每日删除物品大多没有 ExpirationDate,所以那个函数不能在期限为空时提前返回。
func TestApplyCurrentPVFItemExpirationStampsDailyDeleteWithoutAnExpirationDate(t *testing.T) {
now := time.Date(2026, 9, 16, 9, 0, 0, 0, time.UTC)
stack, changed := applyCurrentPVFItemExpirationAt(
dnfrepo.ItemStack{ItemID: 490001382},
dungeonDropItemDefinition{ItemID: 490001382, Kind: dungeonDropItemStackable, DailyDeleteItem: true},
now,
)
if !changed {
t.Fatal("granting a daily-delete item must mark the stack dirty")
}
want := premium.ServiceDayID(now)
if got := stack.Extra[currentDailyDeleteItemStateKey]; got == "" {
t.Fatalf("daily-delete stamp is missing; extra = %v", stack.Extra)
} else if due, _ := currentDailyDeleteItemDue(stack, want); due {
t.Fatal("an item granted today must not already be due")
}
if due, _ := currentDailyDeleteItemDue(stack, want+1); !due {
t.Fatal("the item must be due after the next 06:00 boundary")
}
}
// 普通物品不该因为这条路径多出任何 Extra 键。
func TestApplyCurrentPVFItemExpirationLeavesOrdinaryItemsAlone(t *testing.T) {
stack, _ := applyCurrentPVFItemExpirationAt(
dnfrepo.ItemStack{ItemID: 3011},
dungeonDropItemDefinition{ItemID: 3011, Kind: dungeonDropItemStackable},
time.Date(2026, 9, 16, 9, 0, 0, 0, time.UTC),
)
if _, present := stack.Extra[currentDailyDeleteItemStateKey]; present {
t.Fatalf("ordinary item picked up a daily-delete stamp; extra = %v", stack.Extra)
}
}
// 真 PVF 冒烟:玩家报的那两件必须被解析成每日删除,而且它们身上**没有**期限字段
// —— 这正是"客户端显示 1 天、服务端却毫无察觉"的原因。
func TestRealPVFDailyDeleteItemsAreParsed(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to validate daily-delete parsing")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
catalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
for itemID, name := range map[uint32]string{
490001382: "公会祝福礼盒",
490001383: "魔界抗疲劳秘药",
} {
definition, err := catalog.ResolveItem(itemID)
if err != nil {
t.Fatalf("resolve %d (%s): %v", itemID, name, err)
}
if !definition.DailyDeleteItem {
t.Fatalf("%d (%s) must parse as a daily-delete item", itemID, name)
}
// 本服屏蔽了期限字段,所以这两件不该有 ExpirationDate/UsablePeriodDays:
// 每日删除必须独立于它们生效,不能挂在期限那条通道上。
if !definition.ExpirationDate.IsZero() || definition.UsablePeriodDays != 0 {
t.Fatalf("%d (%s) unexpectedly carries an expiry: date=%v days=%d",
itemID, name, definition.ExpirationDate, definition.UsablePeriodDays)
}
}
// 普通消耗品不该被误判。
if definition, err := catalog.ResolveItem(3011); err == nil && definition.DailyDeleteItem {
t.Fatal("an ordinary material must not parse as daily-delete")
}
}