DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,308 @@
package premium
import (
"bytes"
"reflect"
"strconv"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func TestUpsertFreshRenewAndExpiredStack(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := &dnfrepo.AccountRecord{}
Upsert(account, TypeOverEquip, 86400, 1, now)
if got, want := ExpireAt(*account, TypeOverEquip), now.Unix()+86400; got != want {
t.Fatalf("fresh expire = %d, want %d", got, want)
}
// Renewal while active stacks on the current expiry, not on now.
Upsert(account, TypeOverEquip, 3*86400, 1, now)
if got, want := ExpireAt(*account, TypeOverEquip), now.Unix()+4*86400; got != want {
t.Fatalf("renew expire = %d, want %d", got, want)
}
// Renewal after expiry restarts from now.
later := now.Add(10 * 86400 * time.Second)
Upsert(account, TypeOverEquip, 86400, 1, later)
if got, want := ExpireAt(*account, TypeOverEquip), later.Unix()+86400; got != want {
t.Fatalf("expired renew expire = %d, want %d", got, want)
}
// Count multiplies the duration.
Upsert(account, TypeOverSkill, 86400, 3, now)
if got, want := ExpireAt(*account, TypeOverSkill), now.Unix()+3*86400; got != want {
t.Fatalf("count expire = %d, want %d", got, want)
}
}
func TestAuctionUpsertIncludesPrivateStoreLicenseForTheFullRenewedTerm(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := &dnfrepo.AccountRecord{Metadata: map[string]string{
MetadataKey(TypeAuction): strconv.FormatInt(now.Add(24*time.Hour).Unix(), 10),
}}
Upsert(account, TypeAuction, int64(24*time.Hour/time.Second), 1, now)
want := now.Add(48 * time.Hour).Unix()
if got := ExpireAtRaw(*account, TypeAuction); got != want {
t.Fatalf("auction expire=%d want=%d", got, want)
}
if got := ExpireAtRaw(*account, TypePrivateStore); got != want {
t.Fatalf("private-store expire=%d want bundled auction expiry=%d", got, want)
}
if got := ActivationTypes(TypeAuction); !reflect.DeepEqual(got, []int64{TypeAuction, TypePrivateStore}) {
t.Fatalf("auction activation types=%v", got)
}
}
func TestActiveAndExpireAtMalformed(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{
MetadataKey(TypeCrystal): "garbage",
MetadataKey(TypeLethe): "-5",
}}
if ExpireAt(account, TypeCrystal) != 0 || ExpireAt(account, TypeLethe) != 0 {
t.Fatalf("malformed metadata must read as 0: %+v", account.Metadata)
}
if Active(account, TypeCrystal, now) || Active(account, TypeLethe, now) || Active(account, TypeOverEquip, now) {
t.Fatal("inactive premiums must not report active")
}
}
func TestSelectAckEntriesFoldsDevilSlots(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{}}
Upsert(&account, TypeOverEquip, 86400, 1, now)
Upsert(&account, DevilSlotType(DevilSlotAutoRepair), 3*86400, 1, now)
Upsert(&account, DevilSlotType(DevilSlotGoldCard), 86400, 1, now)
// Expired devil slot must not win the fold.
account.Metadata[MetadataKey(DevilSlotType(DevilSlotQuestHelper))] = "1"
got := SelectAckEntries(account, now)
if got[0] != 2 {
t.Fatalf("count = %d, want 2 (over-equip + folded devil): % X", got[0], got)
}
if len(got) != 1+2*9 {
t.Fatalf("len = %d, want %d", len(got), 1+2*9)
}
if got[1] != byte(TypeOverEquip) {
t.Fatalf("first entry type = %d, want %d", got[1], TypeOverEquip)
}
if got[10] != byte(DevilFolded) {
t.Fatalf("second entry type = %d, want %d", got[10], DevilFolded)
}
var remaining int64
for i := 0; i < 8; i++ {
remaining |= int64(got[11+i]) << (8 * i)
}
if want := int64(3 * 86400); remaining != want {
t.Fatalf("folded devil remaining = %d, want %d", remaining, want)
}
empty := SelectAckEntries(dnfrepo.AccountRecord{}, now)
if !bytes.Equal(empty, []byte{0}) {
t.Fatalf("empty entries = % X, want [0]", empty)
}
}
func TestSelectAckEntriesKeepsAllActivatedRuntimePVFContractTypes(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{
MetadataKey(100): "1700259200",
MetadataKey(29): "1700086400",
MetadataKey(80): "1700172800",
// Malformed, expired, synthetic folded and out-of-wire-range keys must
// not become select-character premium rows.
MetadataKey(88): "garbage",
MetadataKey(92): "1",
MetadataKey(DevilFolded): "1700086400",
MetadataKey(256): "1700086400",
}}
got := SelectAckEntries(account, now)
if got[0] != 4 {
t.Fatalf("count = %d, want private-store compatibility + 3 generic runtime-PVF contracts: % X", got[0], got)
}
gotTypes := []byte{got[1], got[10], got[19], got[28]}
wantTypes := []byte{byte(TypePrivateStore), 29, 80, 100}
if !bytes.Equal(gotTypes, wantTypes) {
t.Fatalf("types = %v, want deterministic %v; body=% X", gotTypes, wantTypes, got)
}
}
func TestSelectAckEntriesCountsTheFourCurrentCouponFamilies(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{
// PVF type 29 is an ordinary contract and stays independent from Black
// Diamond storage type 1000 / wire type 56.
MetadataKey(29): "1700086400",
MetadataKey(TypeOverEquip): "1700086400",
MetadataKey(TypeOverSkill): "1700086400",
MetadataKey(TypeBonusExp): "1700086400",
}}
got := SelectAckEntries(account, now)
if got[0] != 5 || len(got) != 1+5*9 {
t.Fatalf("coupon count/body = %d/%d, want 5/%d: % X", got[0], len(got), 1+5*9, got)
}
gotTypes := []byte{got[1], got[10], got[19], got[28], got[37]}
wantTypes := []byte{byte(TypeOverEquip), byte(TypeOverSkill), byte(TypeBonusExp), byte(TypePrivateStore), 29}
if !bytes.Equal(gotTypes, wantTypes) {
t.Fatalf("coupon types = %v, want %v; body=% X", gotTypes, wantTypes, got)
}
}
func TestBuildActivatedBodyExactBytes(t *testing.T) {
got := BuildActivatedBody(TypeOverEquip, 86400)
want := []byte{2, 0, 22, 0x80, 0x51, 0x01, 0, 0, 0, 0, 0}
if !bytes.Equal(got, want) {
t.Fatalf("body = % X, want % X", got, want)
}
}
func TestCanNotifyActivationRejectsInternalDevilSlots(t *testing.T) {
if !CanNotifyActivation(TypeOverEquip) {
t.Fatal("ordinary u8 premium type must use the current activation notification")
}
for slot := int64(0); slot < DevilSlotCount; slot++ {
if CanNotifyActivation(DevilSlotType(slot)) {
t.Fatalf("internal devil slot %d type=%d must not be truncated into class0/op66", slot, DevilSlotType(slot))
}
}
if CanNotifyActivation(0) || CanNotifyActivation(256) {
t.Fatal("zero and values outside u8 must not use the current activation notification")
}
}
func TestBlackDiamondUsesOnlyDedicatedEntitlementAndProjectsOneWireRow(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{
MetadataKey(1): "1700086400",
MetadataKey(17): "1700086400",
MetadataKey(29): "1700086400",
MetadataKey(TypeBlackDiamondEntitlement): "1700043200",
}}
if got := BlackDiamondExpireAt(account); got != 1_700_043_200 {
t.Fatalf("dedicated Black Diamond expiry=%d", got)
}
entries := SelectAckEntries(account, now)
if len(entries) != 1+5*9 || entries[0] != 5 {
t.Fatalf("premium select projection=% X, want five rows including bundled private-store compatibility", entries)
}
wantTypes := []byte{byte(TypeBlackDiamondWire), 1, byte(TypePrivateStore), 17, 29}
gotTypes := []byte{entries[1], entries[10], entries[19], entries[28], entries[37]}
if !bytes.Equal(gotTypes, wantTypes) {
t.Fatalf("premium select types=%v, want %v; body=% X", gotTypes, wantTypes, entries)
}
Upsert(&account, 29, 86400, 1, now)
if got := ExpireAtRaw(account, 29); got != 1_700_172_800 {
t.Fatalf("PVF type-29 renewal expiry=%d", got)
}
if got := ExpireAtRaw(account, TypePrivateStore); got != 1_700_172_800 {
t.Fatalf("type-29 renewal did not align private-store expiry=%d", got)
}
if got := ExpireAtRaw(account, TypeBlackDiamondEntitlement); got != 1_700_043_200 {
t.Fatalf("type-29 renewal changed Black Diamond expiry=%d", got)
}
}
func TestBuildServiceDataBodyDevilOffsets(t *testing.T) {
now := time.Unix(1_700_000_000, 0)
account := dnfrepo.AccountRecord{Metadata: map[string]string{
MetadataKey(DevilSlotType(DevilSlotAutoRepair)): "1700604800",
MetadataKey(DevilSlotType(DevilSlotGoldCard)): "1699999999",
}}
character := dnfrepo.CharacterRecord{Stats: map[string]int64{
dailyUsageDayKey: ServiceDayID(now),
DailyUsageKey(DevilSlotAutoRepair): 4,
}}
got := BuildServiceDataBody(account, character, now, ServiceTypePanel)
if len(got) != 3+74 {
t.Fatalf("len = %d, want 77", len(got))
}
if got[0] != 1 || got[1] != 1 || got[2] != 0 {
t.Fatalf("prefix = % X, want 01 01 00", got[:3])
}
// serviceType is a u16 at body[1:3]; the silent variant the client itself
// asks with must not look like the panel variant.
if silent := BuildServiceDataBody(account, character, now, ServiceTypeSilent); len(silent) != 3+74 ||
silent[0] != 1 || silent[1] != 4 || silent[2] != 0 {
t.Fatalf("silent prefix = % X, want 01 04 00", silent[:3])
}
offset := 3 + 6 + DevilSlotAutoRepair*9
var expireAt uint32
for i := 0; i < 4; i++ {
expireAt |= uint32(got[int(offset)+i]) << (8 * i)
}
if expireAt != 1_700_604_800 {
t.Fatalf("slot6 absolute expiry = %d, want %d", expireAt, 1_700_604_800)
}
usedOffset := 3 + 10 + DevilSlotAutoRepair*9
var used uint32
for i := 0; i < 4; i++ {
used |= uint32(got[int(usedOffset)+i]) << (8 * i)
}
if used != 4 {
t.Fatalf("slot6 used = %d, want 4", used)
}
// Expired and unused slots stay zero.
other := 3 + 6 + DevilSlotGoldCard*9
if got[other] != 0 || got[other+1] != 0 || got[other+2] != 0 || got[other+3] != 0 {
t.Fatalf("slot0 expired absolute expiry must be zero: % X", got[other:other+4])
}
}
func TestDevilServiceDurationAndDailyUsageRules(t *testing.T) {
now := time.Date(2026, time.July, 27, 5, 59, 59, 0, time.FixedZone("CST", 8*60*60))
account := &dnfrepo.AccountRecord{}
Upsert(account, DevilSlotType(DevilSlotGoldCard), 7*24*60*60, 1, now)
Upsert(account, DevilSlotType(DevilSlotGoldCard), 30*24*60*60, 1, now)
if got, want := ExpireAt(*account, DevilSlotType(DevilSlotGoldCard)), now.Unix()+37*24*60*60; got != want {
t.Fatalf("7-day plus 30-day expiry = %d, want %d", got, want)
}
character := &dnfrepo.CharacterRecord{}
for count := int64(0); count < DailyLimit(DevilSlotGoldCard); count++ {
if !TryConsumeDaily(character, DevilSlotGoldCard, now) {
t.Fatalf("consume %d unexpectedly rejected", count+1)
}
}
if TryConsumeDaily(character, DevilSlotGoldCard, now) {
t.Fatal("eleventh gold-card use must be rejected")
}
if got := DailyUsage(*character, DevilSlotGoldCard, now); got != 10 {
t.Fatalf("gold-card usage = %d, want 10", got)
}
afterReset := now.Add(time.Second)
if got := DailyUsage(*character, DevilSlotGoldCard, afterReset); got != 0 {
t.Fatalf("usage after 06:00 reset = %d, want 0", got)
}
if !TryConsumeDaily(character, DevilSlotGoldCard, afterReset) {
t.Fatal("first use after reset must be accepted")
}
if got := DailyUsage(*character, DevilSlotGoldCard, afterReset); got != 1 {
t.Fatalf("usage after reset consume = %d, want 1", got)
}
}
func TestDailyLimitsMatchPVF(t *testing.T) {
want := map[int64]int64{
DevilSlotGoldCard: 10,
DevilSlotDoubleJar: 8,
DevilSlotQuestHelper: 0,
DevilSlotSevenBuff: 10,
DevilSlotHpMpRecover: 0,
DevilSlotFreeWeakness: 10,
DevilSlotAutoRepair: 6,
DevilSlotFastJarOpen: 0,
}
for slot, limit := range want {
if got := DailyLimit(slot); got != limit {
t.Fatalf("slot %d limit = %d, want %d", slot, got, limit)
}
}
}