Files
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

160 lines
6.1 KiB
Go

package equip
import (
"context"
"encoding/binary"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/premium"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
)
// 魔王契约「自动修理」的服务端触发语义(2026-09-14):
// - 契约生效且该角色当天还有额度 -> 全身免费修满, 次数 +1;
// - 没有契约 -> 空操作(ContractSkipped), 绝不落成收费修理;
// - 当天 6 次用完 -> 同样空操作, 不修也不扣钱。
func devilAutoRepairRaw(itemID int64, durability uint16) []byte {
raw := make([]byte, currentEquipmentRawEntryWireSize)
binary.LittleEndian.PutUint32(raw[2:6], uint32(itemID))
binary.LittleEndian.PutUint16(raw[0x0B:0x0D], durability)
return raw
}
func devilAutoRepairDurability(t *testing.T, ctx context.Context, repos dnfrepo.Group, key string) uint16 {
t.Helper()
record, found, err := repos.Equipment.Load(ctx, "77")
if err != nil || !found {
t.Fatalf("load equipment found=%t err=%v", found, err)
}
entry, ok := record.Entries[key]
if !ok {
t.Fatalf("equipment key %q missing", key)
}
return binary.LittleEndian.Uint16(entry.RawEntry[0x0B:0x0D])
}
func devilAutoRepairDamage(t *testing.T, ctx context.Context, repos dnfrepo.Group, key string, durability uint16) {
t.Helper()
record, found, err := repos.Equipment.Load(ctx, "77")
if err != nil || !found {
t.Fatalf("load equipment found=%t err=%v", found, err)
}
entry := record.Entries[key]
entry.RawEntry = append([]byte(nil), entry.RawEntry...)
binary.LittleEndian.PutUint16(entry.RawEntry[0x0B:0x0D], durability)
record.Entries[key] = entry
if err := repos.Equipment.Save(ctx, record); err != nil {
t.Fatalf("save damaged equipment: %v", err)
}
}
func TestRepairAllContractAutoRepairNeverChargesAndStopsAtDailyLimit(t *testing.T) {
ctx := context.Background()
const (
characterID = "77"
accountID = "acc-devil-auto"
)
repos := dnfrepomemory.NewMemoryGroup()
saveTestCharacter(t, ctx, repos, dnfrepo.CharacterRecord{
CharacterID: characterID,
AccountID: accountID,
Stats: map[string]int64{"gold": 1234},
})
saveTestEquipment(t, ctx, repos, dnfrepo.EquipmentRecord{
CharacterID: characterID,
Entries: map[string]dnfrepo.EquipmentEntry{
"12": {SlotIndex: 12, ItemID: 700, RawEntry: devilAutoRepairRaw(700, 0)},
"14": {SlotIndex: 14, ItemID: 701, RawEntry: devilAutoRepairRaw(701, 10)},
},
})
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{CharacterID: characterID, Slots: map[string]dnfrepo.ItemStack{}})
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: accountID}); err != nil {
t.Fatalf("save account: %v", err)
}
resolver := repairAllTestResolver(map[int64]string{700: "[weapon]", 701: "[coat]"})
owner, err := NewOwner(repos)
if err != nil {
t.Fatalf("NewOwner error = %v", err)
}
command := RepairCommand{
SelectedCharacterID: 77,
AccountID: accountID,
SlotIndex: -1,
AutoRepair: true,
ContractAutoRepair: true,
}
// 1) 没有契约: 空操作, 不修也不扣钱。
skipped, err := owner.RepairAll(ctx, command, resolver)
if err != nil {
t.Fatalf("no-contract RepairAll error = %v", err)
}
if !skipped.ContractSkipped || skipped.FreeRepair || skipped.RepairedCount != 0 || skipped.UpdatedGold != 1234 {
t.Fatalf("no-contract result = %+v, want ContractSkipped with untouched gold", skipped)
}
if got := devilAutoRepairDurability(t, ctx, repos, "14"); got != 10 {
t.Fatalf("no-contract durability = %d, want untouched 10", got)
}
// 2) 契约生效: 全身修满, 免费, 次数 +1。
account, found, err := repos.Account.Load(ctx, accountID)
if err != nil || !found {
t.Fatalf("load account found=%t err=%v", found, err)
}
premium.Upsert(&account, premium.DevilSlotType(premium.DevilSlotAutoRepair), 7*24*60*60, 1, time.Now().UTC())
if err := repos.Account.Save(ctx, account); err != nil {
t.Fatalf("save contract account: %v", err)
}
free, err := owner.RepairAll(ctx, command, resolver)
if err != nil {
t.Fatalf("contract RepairAll error = %v", err)
}
if !free.FreeRepair || free.ContractSkipped || free.Cost != 0 || free.UpdatedGold != 1234 || free.RepairedCount != 2 {
t.Fatalf("contract result = %+v, want free repair of 2 targets at 1234 gold", free)
}
for _, key := range []string{"12", "14"} {
if got := devilAutoRepairDurability(t, ctx, repos, key); got != 20 {
t.Fatalf("slot %s durability = %d, want 20 (PVF max)", key, got)
}
}
character, _, _ := repos.Character.Load(ctx, characterID)
if used := premium.DailyUsage(character, premium.DevilSlotAutoRepair, time.Now().UTC()); used != 1 {
t.Fatalf("daily usage = %d, want 1", used)
}
if character.Stats["gold"] != 1234 {
t.Fatalf("gold = %d, want untouched 1234", character.Stats["gold"])
}
// 3) 把当天 6 次用完, 第 7 次必须是空操作: 不修、不收费。
dailyLimit := int(premium.DailyLimit(premium.DevilSlotAutoRepair))
for round := 1; round <= dailyLimit; round++ {
devilAutoRepairDamage(t, ctx, repos, "14", 5)
result, repairErr := owner.RepairAll(ctx, command, resolver)
if repairErr != nil {
t.Fatalf("round %d RepairAll error = %v", round, repairErr)
}
if round <= 5 {
if !result.FreeRepair || result.RepairedCount != 1 {
t.Fatalf("round %d result = %+v, want the remaining free repair", round, result)
}
continue
}
if !result.ContractSkipped || result.FreeRepair || result.RepairedCount != 0 {
t.Fatalf("round %d (contract still active at the cap) result = %+v", round, result)
}
}
if got := devilAutoRepairDurability(t, ctx, repos, "14"); got != 5 {
t.Fatalf("daily-limit durability = %d, want untouched 5", got)
}
character, _, _ = repos.Character.Load(ctx, characterID)
if used := premium.DailyUsage(character, premium.DevilSlotAutoRepair, time.Now().UTC()); used != premium.DailyLimit(premium.DevilSlotAutoRepair) {
t.Fatalf("daily usage = %d, want %d", used, premium.DailyLimit(premium.DevilSlotAutoRepair))
}
if character.Stats["gold"] != 1234 {
t.Fatalf("gold after the cap = %d, want untouched 1234 (never a paid fallback)", character.Stats["gold"])
}
}