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,291 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
// The current client packs the op50 mode word as target list (high byte) +
|
||||
// sub mode (low byte). 2026-08-28 live capture for a worn item at slot 14:
|
||||
// mode word 0x0300, target 100080175, material slot 94.
|
||||
func TestDecodeUpgradeItemRequestEquippedModeWord(t *testing.T) {
|
||||
body := []byte{
|
||||
0x00, 0x03,
|
||||
0x0E, 0x00,
|
||||
0x2F, 0x1A, 0xF7, 0x05,
|
||||
0x5E, 0x00,
|
||||
0xFF, 0xFF,
|
||||
0x02, 0x00, 0x00, 0x00,
|
||||
'x', 'y',
|
||||
}
|
||||
parsed, err := DecodeUpgradeItemRequest(body)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeUpgradeItemRequest error = %v", err)
|
||||
}
|
||||
if parsed.TargetListType != listTypeEquipment || parsed.Mode != "reinforce" ||
|
||||
parsed.RawMode != 0x0300 || parsed.TargetSlotIndex != 14 ||
|
||||
parsed.TargetItemTemplateID != 100080175 || parsed.MaterialSlotIndex != 94 {
|
||||
t.Fatalf("parsed = %+v", parsed)
|
||||
}
|
||||
|
||||
ampBody := append([]byte(nil), body...)
|
||||
ampBody[0] = 0x01 // 0x0301: worn list-3 amplify
|
||||
parsedAmp, err := DecodeUpgradeItemRequest(ampBody)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeUpgradeItemRequest amplify error = %v", err)
|
||||
}
|
||||
if parsedAmp.TargetListType != listTypeEquipment || parsedAmp.Mode != "amplify" {
|
||||
t.Fatalf("parsed amplify = %+v", parsedAmp)
|
||||
}
|
||||
|
||||
// The backpack wire is unchanged: 0x0000 / 0x0001 still decode to list 0.
|
||||
bagBody := append([]byte(nil), body...)
|
||||
bagBody[1] = 0x00
|
||||
parsedBag, err := DecodeUpgradeItemRequest(bagBody)
|
||||
if err != nil || parsedBag.TargetListType != listTypeMain || parsedBag.Mode != "reinforce" {
|
||||
t.Fatalf("parsed backpack = %+v err=%v", parsedBag, err)
|
||||
}
|
||||
|
||||
// An unsupported list in the high byte is still refused.
|
||||
badList := append([]byte(nil), body...)
|
||||
badList[1] = 0x07
|
||||
if _, err := DecodeUpgradeItemRequest(badList); err == nil {
|
||||
t.Fatal("list-7 mode word unexpectedly accepted")
|
||||
}
|
||||
// Sub mode 2 is the 高级装备强化机 (live capture 2026-09-11 13:35:37); it
|
||||
// decodes as a reinforce carrying the high-upgrade flag. It was rejected
|
||||
// until then, which is why that machine never worked.
|
||||
highSub := append([]byte(nil), body...)
|
||||
highSub[0] = 0x02
|
||||
parsedHigh, err := DecodeUpgradeItemRequest(highSub)
|
||||
if err != nil || parsedHigh.Mode != "reinforce" || !parsedHigh.HighUpgrade {
|
||||
t.Fatalf("sub mode 2 = %+v err=%v", parsedHigh, err)
|
||||
}
|
||||
// Anything above 2 is still unknown and must fail closed.
|
||||
badSub := append([]byte(nil), body...)
|
||||
badSub[0] = 0x03
|
||||
if _, err := DecodeUpgradeItemRequest(badSub); err == nil {
|
||||
t.Fatal("sub mode 3 unexpectedly accepted")
|
||||
}
|
||||
if parsedBag.HighUpgrade {
|
||||
t.Fatal("ordinary reinforce must not carry the high-upgrade flag")
|
||||
}
|
||||
}
|
||||
|
||||
// upgradeTicketEquippedBody is the live C2S layout for a ticket used on a worn
|
||||
// item: mode word high byte 3.
|
||||
func upgradeTicketEquippedBody(targetSlot int16, targetItemID int32, materialSlot int16) []byte {
|
||||
body := upgradeTicketRequestBody(targetSlot, targetItemID, materialSlot)
|
||||
body[0] = 0x00
|
||||
body[1] = 0x03
|
||||
return body
|
||||
}
|
||||
|
||||
// newEquippedUpgradeTicketCommand targets a worn equipment slot (list 3).
|
||||
func newEquippedUpgradeTicketCommand(mode string, targetSlot int16, targetItemID int32, materialSlot int16) Command {
|
||||
cmd := newUpgradeTicketCommand(mode, targetSlot, targetItemID, materialSlot)
|
||||
cmd.TargetListType = listTypeEquipment
|
||||
return cmd
|
||||
}
|
||||
|
||||
// saveEquippedUpgradeTicketFixture puts the worn target into the equipment
|
||||
// record (slot 14) and the ticket/material into the backpack.
|
||||
func saveEquippedUpgradeTicketFixture(t *testing.T, ctx context.Context, repos dnfrepo.Group, targetLevel byte, ticketCount int64) {
|
||||
t.Helper()
|
||||
targetRaw := make([]byte, currentItemListEntrySize)
|
||||
targetRaw[0x0A] = targetLevel
|
||||
ticketRaw := make([]byte, currentItemListEntrySize)
|
||||
if err := repos.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
|
||||
CharacterID: "77",
|
||||
Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"14": {
|
||||
SlotIndex: 14,
|
||||
ItemID: 700,
|
||||
RawEntry: targetRaw,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "equipment",
|
||||
"durability": "45",
|
||||
"max_durability": "45",
|
||||
"current_exe_inventory_count": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("Save equipment error = %v", err)
|
||||
}
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 121): {
|
||||
ItemID: 9100,
|
||||
Count: ticketCount,
|
||||
RawEntry: ticketRaw,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "stackable",
|
||||
"pvf_path": "stackable/ticket/plus10.stk",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestOwnerUpgradeTicketOnEquippedTargetWritesEquipmentRepo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEquippedUpgradeTicketFixture(t, ctx, repos, 2, 2)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.UpgradeTicket(ctx, newEquippedUpgradeTicketCommand("reinforce", 14, 700, 121), reinforceTicketResolver(10, 100000))
|
||||
if err != nil {
|
||||
t.Fatalf("UpgradeTicket error = %v", err)
|
||||
}
|
||||
if !result.Success || !result.UpgradeSucceeded || result.TargetListType != listTypeEquipment || result.NewLevel != 10 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
equip, ok, err := repos.Equipment.Load(ctx, "77")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load equipment found=%t err=%v", ok, err)
|
||||
}
|
||||
entry := equip.Entries["14"]
|
||||
if got := entry.RawEntry[0x0A] & 0x1F; got != 10 {
|
||||
t.Fatalf("equipped raw level = %d, want 10", got)
|
||||
}
|
||||
if got := entry.Extra["reinforce"]; got != "10" {
|
||||
t.Fatalf("equipped extra reinforce = %q, want 10", got)
|
||||
}
|
||||
// The ticket is consumed from the backpack, and no list-3 key leaked in.
|
||||
loaded := loadTestInventory(t, ctx, repos, "77")
|
||||
if got := loaded.Slots[slotKey(listTypeMain, 121)].Count; got != 1 {
|
||||
t.Fatalf("ticket count = %d, want 1", got)
|
||||
}
|
||||
if _, leaked := loaded.Slots[slotKey(listTypeEquipment, 14)]; leaked {
|
||||
t.Fatalf("worn row leaked into the backpack record: %+v", loaded.Slots)
|
||||
}
|
||||
if result.TargetUpdatedStack.ItemID != 700 || result.TargetUpdatedStack.RawEntry[0x0A]&0x1F != 10 {
|
||||
t.Fatalf("target updated stack = %+v", result.TargetUpdatedStack)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerUpgradeTicketOnEquippedTargetMissingEntryFailsInvalidTarget(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEquippedUpgradeTicketFixture(t, ctx, repos, 2, 2)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.UpgradeTicket(ctx, newEquippedUpgradeTicketCommand("reinforce", 99, 700, 121), reinforceTicketResolver(10, 100000))
|
||||
if err != nil {
|
||||
t.Fatalf("UpgradeTicket error = %v", err)
|
||||
}
|
||||
if result.Success || result.ErrorCode != upgradeTicketErrorInvalidTarget || result.Changed {
|
||||
t.Fatalf("result = %+v, want invalid target without mutation", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerUpgradeReinforceOnEquippedTargetWritesEquipmentRepo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveTestCharacter(t, ctx, repos, dnfrepo.CharacterRecord{CharacterID: "77", AccountID: "acc", Stats: map[string]int64{"gold": 10_000_000}})
|
||||
saveEquippedUpgradeTicketFixture(t, ctx, repos, 2, 3)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := NewUpgradeCommand(alignedcmd.Request{AccountID: "acc", SelectedCharacterID: 77}, UpgradeItemRequest{
|
||||
Mode: "reinforce",
|
||||
TargetListType: listTypeEquipment,
|
||||
TargetSlotIndex: 14,
|
||||
TargetItemTemplateID: 700,
|
||||
MaterialSlotIndex: 121,
|
||||
OptionalTicketSlotIndex: -1,
|
||||
})
|
||||
cmd.UpgradeSuccessWeight = 100000 // guaranteed success
|
||||
result, err := owner.Upgrade(ctx, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Upgrade error = %v", err)
|
||||
}
|
||||
if !result.Success || !result.Changed || result.TargetListType != listTypeEquipment ||
|
||||
result.OldLevel != 2 || result.NewLevel != 3 || result.MaterialRemainingStackCount != 2 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
equip, ok, err := repos.Equipment.Load(ctx, "77")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load equipment found=%t err=%v", ok, err)
|
||||
}
|
||||
entry := equip.Entries["14"]
|
||||
if got := entry.RawEntry[0x0A] & 0x1F; got != 3 {
|
||||
t.Fatalf("equipped raw level = %d, want 3", got)
|
||||
}
|
||||
if got := entry.Extra["reinforce"]; got != "3" {
|
||||
t.Fatalf("equipped extra reinforce = %q, want 3", got)
|
||||
}
|
||||
loaded := loadTestInventory(t, ctx, repos, "77")
|
||||
if got := loaded.Slots[slotKey(listTypeMain, 121)].Count; got != 2 {
|
||||
t.Fatalf("material count = %d, want 2", got)
|
||||
}
|
||||
if _, leaked := loaded.Slots[slotKey(listTypeEquipment, 14)]; leaked {
|
||||
t.Fatalf("worn row leaked into the backpack record: %+v", loaded.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerUpgradeTicketOnEquippedTargetSendsAckAndEquipmentRefresh(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEquippedUpgradeTicketFixture(t, ctx, repos, 2, 2)
|
||||
|
||||
got := handleUpgradeTicketRequest(t, repos, reinforceTicketResolver(10, 100000), upgradeTicketEquippedBody(14, 700, 121))
|
||||
if !got.Handled || !got.ResponseAllowed {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
// ACK + the material row in list 0; the worn row is not in that refresh.
|
||||
if len(got.UpperResponses) != 2 {
|
||||
t.Fatalf("responses = %d, want ack + material refresh", len(got.UpperResponses))
|
||||
}
|
||||
ack := got.UpperResponses[0]
|
||||
if ack.MsgID != uint16(dnfenum.CmdPacketUpgradeItem) || ack.Classification != 1 {
|
||||
t.Fatalf("ack = msg 0x%04X class %d", ack.MsgID, ack.Classification)
|
||||
}
|
||||
if len(ack.Body) < 15 || ack.Body[14] != listTypeEquipment {
|
||||
t.Fatalf("ack body = %x, want byte[14] = listTypeEquipment(3)", ack.Body)
|
||||
}
|
||||
refresh := got.UpperResponses[1]
|
||||
if refresh.MsgID != msgItemListUpdate || refresh.Classification != 0 || len(refresh.Body) < 2 || refresh.Body[0] != listTypeMain || refresh.Body[1] != 1 {
|
||||
t.Fatalf("refresh = msg 0x%04X class %d body=%x, want one list-0 material row", refresh.MsgID, refresh.Classification, refresh.Body)
|
||||
}
|
||||
if len(got.EquipmentSlotRefreshes) != 1 || got.EquipmentSlotRefreshes[0] != 14 || len(got.ItemSlotRefreshes) != 0 {
|
||||
t.Fatalf("equipment refreshes = %v item refreshes = %+v, want the single worn slot 14", got.EquipmentSlotRefreshes, got.ItemSlotRefreshes)
|
||||
}
|
||||
equip, ok, err := repos.Equipment.Load(ctx, "77")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load equipment found=%t err=%v", ok, err)
|
||||
}
|
||||
if got := equip.Entries["14"].RawEntry[0x0A] & 0x1F; got != 10 {
|
||||
t.Fatalf("equipped raw level = %d, want 10", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The backpack path must be byte-for-byte what it was: list 0 in the mode word
|
||||
// still resolves the target in list 0, and byte[14] of the ACK stays 0.
|
||||
func TestHandlerUpgradeTicketOnBackpackTargetKeepsByte14Zero(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveUpgradeTicketFixture(t, ctx, repos, nil, 2, 2)
|
||||
got := handleUpgradeTicketRequest(t, repos, reinforceTicketResolver(10, 100000), upgradeTicketRequestBody(9, 700, 121))
|
||||
if !got.Handled || len(got.UpperResponses) != 2 {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
if ack := got.UpperResponses[0]; len(ack.Body) < 15 || ack.Body[14] != 0 {
|
||||
t.Fatalf("backpack ack body = %x, want byte[14] = 0", ack.Body)
|
||||
}
|
||||
if len(got.EquipmentSlotRefreshes) != 0 {
|
||||
t.Fatalf("backpack target must not request an equipment refresh: %v", got.EquipmentSlotRefreshes)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user