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,610 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
func staticEnchantResolver(resolution alignedcmd.EnchantBeadResolution) alignedcmd.EnchantBeadResolver {
|
||||
return func(beadItemID int64, targetItemID int64) (alignedcmd.EnchantBeadResolution, error) {
|
||||
return resolution, nil
|
||||
}
|
||||
}
|
||||
|
||||
func coatEnchantResolver(cardItemID int64, counts []int64) alignedcmd.EnchantBeadResolver {
|
||||
return staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: cardItemID,
|
||||
AllowedEquipmentTypes: []string{"[coat]"},
|
||||
UpgradeCounts: counts,
|
||||
TargetEquipmentType: "[coat]",
|
||||
TargetKind: "equipment",
|
||||
})
|
||||
}
|
||||
|
||||
func creatureEnchantResolver(cardItemID int64, targetItemID int64) alignedcmd.EnchantBeadResolver {
|
||||
return staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: cardItemID,
|
||||
TargetWhitelist: []int64{targetItemID},
|
||||
AllowedEquipmentTypes: []string{"[creature]"},
|
||||
TargetEquipmentType: "[creature]",
|
||||
TargetKind: "equipment",
|
||||
})
|
||||
}
|
||||
|
||||
func newEnchantTestCommand(beadSlot int16, targetSlot int16) Command {
|
||||
return NewEnchantCommand(alignedcmd.Request{
|
||||
AccountID: "acc",
|
||||
SelectedCharacterID: 77,
|
||||
}, EnchantByBeadRequest{
|
||||
BeadListType: listTypeMain,
|
||||
BeadSlotIndex: beadSlot,
|
||||
TargetListType: listTypeMain,
|
||||
TargetSlotIndex: targetSlot,
|
||||
})
|
||||
}
|
||||
|
||||
func saveEnchantFixture(t *testing.T, ctx context.Context, repos dnfrepo.Group, beadCount int64, beadRaw byte, targetCardID int64) {
|
||||
t.Helper()
|
||||
beadRawEntry := make([]byte, currentItemListEntrySize)
|
||||
beadRawEntry[0x12] = beadRaw
|
||||
targetRawEntry := make([]byte, currentItemListEntrySize)
|
||||
targetRawEntry[0x0E] = byte(targetCardID)
|
||||
targetRawEntry[0x0F] = byte(targetCardID >> 8)
|
||||
targetRawEntry[0x10] = byte(targetCardID >> 16)
|
||||
targetRawEntry[0x11] = byte(targetCardID >> 24)
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 12): {
|
||||
ItemID: 8801,
|
||||
Count: beadCount,
|
||||
RawEntry: beadRawEntry,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "stackable",
|
||||
"pvf_path": "stackable/bead/fire_bead.stk",
|
||||
},
|
||||
},
|
||||
slotKey(listTypeMain, 30): {
|
||||
ItemID: 700,
|
||||
Count: 1,
|
||||
RawEntry: targetRawEntry,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "equipment",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func savePetEnchantFixture(t *testing.T, ctx context.Context, repos dnfrepo.Group, beadCount int64) {
|
||||
t.Helper()
|
||||
const (
|
||||
petItemID int64 = 400990168
|
||||
petSlot int16 = 24
|
||||
)
|
||||
petRaw := make([]byte, currentItemListEntrySize)
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 12): {
|
||||
ItemID: 490007163,
|
||||
Count: beadCount,
|
||||
Extra: map[string]string{
|
||||
"item_kind": "stackable",
|
||||
},
|
||||
},
|
||||
slotKey(listTypePet, petSlot): {
|
||||
ItemID: petItemID,
|
||||
Count: 1,
|
||||
RawEntry: petRaw,
|
||||
Extra: map[string]string{
|
||||
"creature_serial_or_handle": "37",
|
||||
"creature_key": "37",
|
||||
"item_kind": "equipment",
|
||||
"equipment_type": "[creature]",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := repos.Pet.Save(ctx, dnfrepo.PetRecord{
|
||||
CharacterID: "77",
|
||||
Entries: map[string]dnfrepo.PetEntry{
|
||||
"37": {
|
||||
PetKey: "37",
|
||||
CreatureKey: 37,
|
||||
ItemID: petItemID,
|
||||
SourceListType: listTypePet,
|
||||
SourceSlotIndex: petSlot,
|
||||
Level: 1,
|
||||
Satiety: 100,
|
||||
RawEntry: make([]byte, currentItemListEntrySize),
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("Save pet error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantAppliesCardAndConsumesBead(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, repos, 2, 0, 0)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, newEnchantTestCommand(12, 30), coatEnchantResolver(9001, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || !result.Changed || result.CardItemID != 9001 || result.EnchantUpgradeCount != 0 || result.BeadRemainingCount != 1 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
loaded := loadTestInventory(t, ctx, repos, "77")
|
||||
target := loaded.Slots[slotKey(listTypeMain, 30)]
|
||||
if got := target.Extra["value_a"]; got != "9001" {
|
||||
t.Fatalf("target value_a = %q, want 9001", got)
|
||||
}
|
||||
if got := target.Extra["byte_12"]; got != "0" {
|
||||
t.Fatalf("target byte_12 = %q, want 0", got)
|
||||
}
|
||||
if got := target.RawEntry[0x0E:0x12]; got[0] != 0x29 || got[1] != 0x23 || got[2] != 0 || got[3] != 0 {
|
||||
t.Fatalf("target raw card id = %x, want 9001 LE", got)
|
||||
}
|
||||
if got := target.RawEntry[0x12]; got != 0 {
|
||||
t.Fatalf("target raw upgrade byte = %d, want 0", got)
|
||||
}
|
||||
bead := loaded.Slots[slotKey(listTypeMain, 12)]
|
||||
if bead.Count != 1 {
|
||||
t.Fatalf("bead count = %d, want 1", bead.Count)
|
||||
}
|
||||
if got := bead.RawEntry[0x06]; got != 1 {
|
||||
t.Fatalf("bead raw amount = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantAppliesCardToWornEquipmentAtomically(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
beadRaw := make([]byte, currentItemListEntrySize)
|
||||
wornRaw := make([]byte, currentItemListEntrySize)
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 12): {ItemID: 10095700, Count: 1, RawEntry: beadRaw},
|
||||
},
|
||||
})
|
||||
if err := repos.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
|
||||
CharacterID: "77",
|
||||
Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"17": {SlotIndex: 17, ItemID: 700, RawEntry: wornRaw, Extra: map[string]string{"equipment_type": "[magic stone]"}},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := newEnchantTestCommand(12, 17)
|
||||
cmd.TargetListType = listTypeEquipment
|
||||
result, err := owner.Enchant(ctx, cmd, staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: 10095701,
|
||||
AllowedEquipmentTypes: []string{"[magic stone]"},
|
||||
TargetEquipmentType: "[magic stone]",
|
||||
TargetKind: "equipment",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.Success || !result.Changed || result.TargetListType != listTypeEquipment || result.TargetSlotIndex != 17 || result.BeadRemainingCount != 0 {
|
||||
t.Fatalf("result=%+v", result)
|
||||
}
|
||||
loadedInventory := loadTestInventory(t, ctx, repos, "77")
|
||||
if _, found := loadedInventory.Slots[slotKey(listTypeMain, 12)]; found {
|
||||
t.Fatal("bead remained after worn equipment enchant")
|
||||
}
|
||||
loadedEquipment, found, err := repos.Equipment.Load(ctx, "77")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load equipment found=%t err=%v", found, err)
|
||||
}
|
||||
worn := loadedEquipment.Entries["17"]
|
||||
if got := binary.LittleEndian.Uint32(worn.RawEntry[0x0E:0x12]); got != 10095701 {
|
||||
t.Fatalf("worn enchant card=%d", got)
|
||||
}
|
||||
if worn.Extra["enchant_card_id"] != "10095701" {
|
||||
t.Fatalf("worn extra=%v", worn.Extra)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantAppliesRandomTitleStateAndClearsOrdinaryEnchant(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, repos, 1, 0, 1234)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, newEnchantTestCommand(12, 30), staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
RandomTitleEnchantConfigID: 10001,
|
||||
RandomTitleEnchantRowID: 7,
|
||||
TargetWhitelist: []int64{700},
|
||||
AllowedEquipmentTypes: []string{"[title name]"},
|
||||
TargetEquipmentType: "[title name]",
|
||||
TargetKind: "equipment",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || result.CardItemID != 0 || result.EnchantUpgradeCount != 0 || result.BeadRemainingCount != 0 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
target := loadTestInventory(t, ctx, repos, "77").Slots[slotKey(listTypeMain, 30)]
|
||||
if target.Extra["random_title_enchant_config_id"] != "10001" || target.Extra["random_title_enchant_row_id"] != "7" ||
|
||||
target.Extra["enchant_card_id"] != "0" || target.Extra["enchant_upgrade_count"] != "0" {
|
||||
t.Fatalf("target random title extra = %+v", target.Extra)
|
||||
}
|
||||
if got := target.RawEntry[0x0E:0x12]; got[0] != 0 || got[1] != 0 || got[2] != 0 || got[3] != 0 || target.RawEntry[0x12] != 0 {
|
||||
t.Fatalf("ordinary enchant fields survived: raw=%x", target.RawEntry[0x0E:0x13])
|
||||
}
|
||||
if got := int64(binary.LittleEndian.Uint16(target.RawEntry[0x61:0x63])); got != 10001 {
|
||||
t.Fatalf("random config raw=%d, want 10001", got)
|
||||
}
|
||||
if got := int64(binary.LittleEndian.Uint16(target.RawEntry[0x63:0x65])); got != 7 {
|
||||
t.Fatalf("random row raw=%d, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantDeletesExhaustedBeadAndOverwritesCard(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, repos, 1, 0, 1234)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, newEnchantTestCommand(12, 30), coatEnchantResolver(9001, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || result.BeadRemainingCount != 0 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
loaded := loadTestInventory(t, ctx, repos, "77")
|
||||
if _, ok := loaded.Slots[slotKey(listTypeMain, 12)]; ok {
|
||||
t.Fatalf("exhausted bead slot should be deleted")
|
||||
}
|
||||
target := loaded.Slots[slotKey(listTypeMain, 30)]
|
||||
if got := target.Extra["value_a"]; got != "9001" {
|
||||
t.Fatalf("overwritten target value_a = %q, want 9001", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantPetCreatureUpdatesInventoryAndTypedPetAtomically(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
savePetEnchantFixture(t, ctx, repos, 1)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := newEnchantTestCommand(12, 24)
|
||||
cmd.TargetListType = listTypePet
|
||||
result, err := owner.Enchant(ctx, cmd, creatureEnchantResolver(10008663, 400990168))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || !result.Changed || result.TargetListType != listTypePet ||
|
||||
result.CardItemID != 10008663 || result.BeadRemainingCount != 0 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
inventory := loadTestInventory(t, ctx, repos, "77")
|
||||
if _, found := inventory.Slots[slotKey(listTypeMain, 12)]; found {
|
||||
t.Fatal("exhausted pet bead was not removed")
|
||||
}
|
||||
target := inventory.Slots[slotKey(listTypePet, 24)]
|
||||
if target.Extra["value_a"] != "10008663" || target.Extra["pet_enchant_card_item_id"] != "10008663" || target.Extra["byte_12"] != "0" {
|
||||
t.Fatalf("pet target extra = %+v", target.Extra)
|
||||
}
|
||||
if got := int64(uint32(target.RawEntry[0x0E]) | uint32(target.RawEntry[0x0F])<<8 | uint32(target.RawEntry[0x10])<<16 | uint32(target.RawEntry[0x11])<<24); got != 10008663 {
|
||||
t.Fatalf("pet target raw card = %d, want 10008663", got)
|
||||
}
|
||||
petRecord, found, err := repos.Pet.Load(ctx, "77")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("Load pet found=%t err=%v", found, err)
|
||||
}
|
||||
pet := petRecord.Entries["37"]
|
||||
if pet.Extra["pet_enchant_card_item_id"] != "10008663" || pet.Extra["pet_enchant_upgrade_count"] != "0" {
|
||||
t.Fatalf("typed pet extra = %+v", pet.Extra)
|
||||
}
|
||||
if got := int64(uint32(pet.RawEntry[0x0E]) | uint32(pet.RawEntry[0x0F])<<8 | uint32(pet.RawEntry[0x10])<<16 | uint32(pet.RawEntry[0x11])<<24); got != 10008663 {
|
||||
t.Fatalf("typed pet raw card = %d, want 10008663", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantEquippedPetCreatureUpdatesEquipmentAndTypedPetAtomically(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
const petItemID int64 = 400990167
|
||||
const petSerial uint32 = 41
|
||||
beadRaw := make([]byte, currentItemListEntrySize)
|
||||
var equippedRaw = make([]byte, 43)
|
||||
equippedRaw[0] = 26
|
||||
binary.LittleEndian.PutUint32(equippedRaw[1:5], uint32(petItemID))
|
||||
binary.LittleEndian.PutUint32(equippedRaw[24:28], petSerial)
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "77", Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 12): {ItemID: 490007163, Count: 1, RawEntry: beadRaw, Extra: map[string]string{"item_kind": "stackable"}},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Equipment.Save(ctx, dnfrepo.EquipmentRecord{CharacterID: "77", Entries: map[string]dnfrepo.EquipmentEntry{
|
||||
"26": {SlotIndex: 26, ItemID: petItemID, RawEntry: equippedRaw, Extra: map[string]string{"item_kind": "equipment", "equipment_type": "creature"}},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Pet.Save(ctx, dnfrepo.PetRecord{CharacterID: "77", EquippedKey: "41", Entries: map[string]dnfrepo.PetEntry{
|
||||
"41": {PetKey: "41", CreatureKey: petSerial, ItemID: petItemID, SourceListType: listTypeEquipment, SourceSlotIndex: 26, RawEntry: make([]byte, currentItemListEntrySize)},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := newEnchantTestCommand(12, 26)
|
||||
cmd.TargetListType = listTypeEquipment
|
||||
result, err := owner.Enchant(ctx, cmd, creatureEnchantResolver(10008713, petItemID))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || result.TargetListType != listTypeEquipment || result.TargetSlotIndex != 26 || result.CardItemID != 10008713 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
inv, _, _ := repos.Inventory.Load(ctx, "77")
|
||||
if _, ok := inv.Slots[slotKey(listTypeMain, 12)]; ok {
|
||||
t.Fatal("equipped pet bead was not consumed")
|
||||
}
|
||||
equipment, _, _ := repos.Equipment.Load(ctx, "77")
|
||||
eq := equipment.Entries["26"]
|
||||
if got := binary.LittleEndian.Uint32(eq.RawEntry[0x10:0x14]); got != 10008713 || eq.Extra["enchant_card_id"] != "10008713" {
|
||||
t.Fatalf("equipped enchant = raw=%d extra=%+v", got, eq.Extra)
|
||||
}
|
||||
pet, _, _ := repos.Pet.Load(ctx, "77")
|
||||
if pet.Entries["41"].Extra["pet_enchant_card_item_id"] != "10008713" {
|
||||
t.Fatalf("typed pet enchant = %+v", pet.Entries["41"].Extra)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantPetCreatureRejectsPVFWhitelistMismatchWithoutMutation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
savePetEnchantFixture(t, ctx, repos, 1)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := newEnchantTestCommand(12, 24)
|
||||
cmd.TargetListType = listTypePet
|
||||
result, err := owner.Enchant(ctx, cmd, creatureEnchantResolver(10008663, 400990006))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if result.Success || result.Changed || result.ErrorCode != enchantErrorInvalidTarget {
|
||||
t.Fatalf("result = %+v, want PVF whitelist rejection", result)
|
||||
}
|
||||
inventory := loadTestInventory(t, ctx, repos, "77")
|
||||
if got := inventory.Slots[slotKey(listTypeMain, 12)].Count; got != 1 {
|
||||
t.Fatalf("bead count mutated on whitelist rejection: %d", got)
|
||||
}
|
||||
if got := inventory.Slots[slotKey(listTypePet, 24)].Extra["value_a"]; got != "" {
|
||||
t.Fatalf("pet target mutated on whitelist rejection: value_a=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantUpgradeCountTable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, repos, 2, 2, 0)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, newEnchantTestCommand(12, 30), coatEnchantResolver(9001, []int64{0, 1, 2}))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if !result.Success || result.EnchantUpgradeCount != 2 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
loaded := loadTestInventory(t, ctx, repos, "77")
|
||||
target := loaded.Slots[slotKey(listTypeMain, 30)]
|
||||
if got := target.RawEntry[0x12]; got != 2 {
|
||||
t.Fatalf("target raw upgrade byte = %d, want 2", got)
|
||||
}
|
||||
|
||||
rejections := []struct {
|
||||
name string
|
||||
resolver alignedcmd.EnchantBeadResolver
|
||||
}{
|
||||
{name: "count outside table", resolver: coatEnchantResolver(9001, []int64{0, 1})},
|
||||
{name: "nonzero count without table", resolver: coatEnchantResolver(9001, nil)},
|
||||
}
|
||||
for _, tc := range rejections {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fresh := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, fresh, 2, 2, 0)
|
||||
freshOwner, err := NewOwner(fresh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := freshOwner.Enchant(ctx, newEnchantTestCommand(12, 30), tc.resolver)
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if result.Success || result.ErrorCode != enchantErrorUnsupported {
|
||||
t.Fatalf("result = %+v, want unsupported 0x17", result)
|
||||
}
|
||||
loaded := loadTestInventory(t, ctx, fresh, "77")
|
||||
if got := loaded.Slots[slotKey(listTypeMain, 12)].Count; got != 2 {
|
||||
t.Fatalf("bead count mutated on rejection: %d", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantRejections(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
mutateCmd func(*Command)
|
||||
resolver alignedcmd.EnchantBeadResolver
|
||||
setup func(t *testing.T, repos dnfrepo.Group)
|
||||
wantCode byte
|
||||
}{
|
||||
{
|
||||
name: "bead outside main list",
|
||||
mutateCmd: func(cmd *Command) {
|
||||
cmd.BeadListType = 1
|
||||
},
|
||||
resolver: coatEnchantResolver(9001, nil),
|
||||
wantCode: enchantErrorUnsupported,
|
||||
},
|
||||
{
|
||||
name: "target outside supported equipment owners",
|
||||
mutateCmd: func(cmd *Command) {
|
||||
cmd.TargetListType = 4
|
||||
},
|
||||
resolver: coatEnchantResolver(9001, nil),
|
||||
wantCode: enchantErrorUnsupported,
|
||||
},
|
||||
{
|
||||
name: "bead slot missing",
|
||||
resolver: coatEnchantResolver(9001, nil),
|
||||
setup: func(t *testing.T, repos dnfrepo.Group) {
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{CharacterID: "77", Slots: map[string]dnfrepo.ItemStack{}})
|
||||
},
|
||||
wantCode: enchantErrorInvalidBead,
|
||||
},
|
||||
{
|
||||
name: "target slot missing",
|
||||
resolver: coatEnchantResolver(9001, nil),
|
||||
setup: func(t *testing.T, repos dnfrepo.Group) {
|
||||
saveTestInventory(t, ctx, repos, dnfrepo.InventoryRecord{
|
||||
CharacterID: "77",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
slotKey(listTypeMain, 12): {ItemID: 8801, Count: 1},
|
||||
},
|
||||
})
|
||||
},
|
||||
wantCode: enchantErrorInvalidTarget,
|
||||
},
|
||||
{
|
||||
name: "bead carries no card",
|
||||
resolver: coatEnchantResolver(0, nil),
|
||||
wantCode: enchantErrorInvalidBead,
|
||||
},
|
||||
{
|
||||
name: "target not equipment kind",
|
||||
resolver: staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: 9001,
|
||||
AllowedEquipmentTypes: []string{"[coat]"},
|
||||
TargetEquipmentType: "[coat]",
|
||||
TargetKind: "stackable",
|
||||
}),
|
||||
wantCode: enchantErrorInvalidTarget,
|
||||
},
|
||||
{
|
||||
name: "target outside bead whitelist",
|
||||
resolver: staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: 9001,
|
||||
TargetWhitelist: []int64{111, 222},
|
||||
AllowedEquipmentTypes: []string{"[coat]"},
|
||||
TargetEquipmentType: "[coat]",
|
||||
TargetKind: "equipment",
|
||||
}),
|
||||
wantCode: enchantErrorInvalidTarget,
|
||||
},
|
||||
{
|
||||
name: "equipment type not on card",
|
||||
resolver: staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: 9001,
|
||||
AllowedEquipmentTypes: []string{"[weapon]"},
|
||||
TargetEquipmentType: "[coat]",
|
||||
TargetKind: "equipment",
|
||||
}),
|
||||
wantCode: enchantErrorInvalidTarget,
|
||||
},
|
||||
{
|
||||
name: "card has no allowed types",
|
||||
resolver: staticEnchantResolver(alignedcmd.EnchantBeadResolution{
|
||||
CardItemID: 9001,
|
||||
TargetEquipmentType: "[coat]",
|
||||
TargetKind: "equipment",
|
||||
}),
|
||||
wantCode: enchantErrorInvalidTarget,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if tc.setup != nil {
|
||||
tc.setup(t, repos)
|
||||
} else {
|
||||
saveEnchantFixture(t, ctx, repos, 2, 0, 0)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := newEnchantTestCommand(12, 30)
|
||||
if tc.mutateCmd != nil {
|
||||
tc.mutateCmd(&cmd)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, cmd, tc.resolver)
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if result.Success || result.ErrorCode != tc.wantCode {
|
||||
t.Fatalf("result = %+v, want error code 0x%02X", result, tc.wantCode)
|
||||
}
|
||||
if result.Changed {
|
||||
t.Fatalf("rejected enchant must not mutate state")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantSameSlotRejected(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
saveEnchantFixture(t, ctx, repos, 2, 0, 0)
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.Enchant(ctx, newEnchantTestCommand(12, 12), coatEnchantResolver(9001, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Enchant error = %v", err)
|
||||
}
|
||||
if result.Success || result.ErrorCode != enchantErrorInvalidTarget {
|
||||
t.Fatalf("result = %+v, want 0x13", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerEnchantNilResolverFailsClosed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := owner.Enchant(ctx, newEnchantTestCommand(12, 30), nil); !errors.Is(err, ErrEnchantResolverRequired) {
|
||||
t.Fatalf("Enchant error = %v, want ErrEnchantResolverRequired", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user