服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
611 lines
21 KiB
Go
611 lines
21 KiB
Go
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)
|
|
}
|
|
}
|