服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
221 lines
7.9 KiB
Go
221 lines
7.9 KiB
Go
package wedding
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
repo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
func capsuleTestPair(t *testing.T) (repo.Group, *Catalog, *CapsuleCatalog, CapsuleCommand) {
|
|
t.Helper()
|
|
repos := memory.NewMemoryGroup()
|
|
cat := &Catalog{MaxLove: 999, MinimumLevel: 30}
|
|
cat.RingTickets[3] = Item{10002613, 1}
|
|
rules := &CapsuleCatalog{MaxOwn: 10, MaxTaken: 10, Gauge: 2, MaxGauge: 10, ItemID: 10002910}
|
|
now := time.Unix(1789200000, 0)
|
|
for i := 1; i <= 2; i++ {
|
|
id := strconv.Itoa(i)
|
|
c := repo.CharacterRecord{CharacterID: id, AccountID: "a" + id, Level: 30, Stats: map[string]int64{"gold": 9000000}}
|
|
store(&c, State{PairID: 65538, PartnerID: uint16(3 - i), Status: 4, Ring: 2, Love: 946, Date: now.Unix() - 86400}, now)
|
|
if err := repos.Character.Save(context.Background(), c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Inventory.Save(context.Background(), repo.InventoryRecord{CharacterID: id, Slots: map[string]repo.ItemStack{}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
cmd := CapsuleCommand{ActorID: "1", PartnerID: "2", AccountID: "a1", Opcode: 1052, Now: now, Grant: func(b *repo.InventoryRecord, item Item) error {
|
|
if b.Slots == nil {
|
|
b.Slots = make(map[string]repo.ItemStack)
|
|
}
|
|
stack := b.Slots["0:57"]
|
|
stack.ItemID = item.ID
|
|
stack.Count += item.Count
|
|
b.Slots["0:57"] = stack
|
|
return nil
|
|
}}
|
|
return repos, cat, rules, cmd
|
|
}
|
|
|
|
func TestWeddingCapsuleWorkflowAndRepeatedClaims(t *testing.T) {
|
|
ctx := context.Background()
|
|
repos, cat, rules, cmd := capsuleTestPair(t)
|
|
for made := 0; made < 2; made++ {
|
|
for clear := 0; clear < 5; clear++ {
|
|
c, _, _ := repos.Character.Load(ctx, "1")
|
|
if err := AccumulateCapsuleClear(&c, rules); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Character.Save(ctx, c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if clear < 4 {
|
|
if _, err := CommitCapsule(ctx, repos, cat, rules, cmd); !errors.Is(err, ErrCapsuleEnergy) {
|
|
t.Fatalf("early make: %v", err)
|
|
}
|
|
}
|
|
}
|
|
res, err := CommitCapsule(ctx, repos, cat, rules, cmd)
|
|
if err != nil || res.States[0].Progress != 0 || res.States[0].Own != int64(made+1) {
|
|
t.Fatalf("make=%+v %v", res.States, err)
|
|
}
|
|
}
|
|
cmd.Opcode = 1053
|
|
res, err := CommitCapsule(ctx, repos, cat, rules, cmd)
|
|
if err != nil || res.States[0].Own != 0 || res.States[1].Taken != 2 {
|
|
t.Fatalf("give=%+v %v", res.States, err)
|
|
}
|
|
if _, err := CommitCapsule(ctx, repos, cat, rules, cmd); !errors.Is(err, ErrCapsuleEmpty) {
|
|
t.Fatalf("repeat give=%v", err)
|
|
}
|
|
// The persisted pending stock survives the next day and has no online-session dependency.
|
|
cmd.ActorID, cmd.PartnerID, cmd.AccountID, cmd.Opcode, cmd.Now = "2", "1", "a2", 1054, cmd.Now.Add(48*time.Hour)
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 2)
|
|
for i := 0; i < 2; i++ {
|
|
wg.Add(1)
|
|
go func() { defer wg.Done(); _, err := CommitCapsule(ctx, repos, cat, rules, cmd); errs <- err }()
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
success := 0
|
|
for err := range errs {
|
|
if err == nil {
|
|
success++
|
|
} else if !errors.Is(err, ErrCapsuleEmpty) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
bag, _, _ := repos.Inventory.Load(ctx, "2")
|
|
c, _, _ := repos.Character.Load(ctx, "2")
|
|
state, _ := ReadCapsule(c, rules)
|
|
if success != 1 || bag.Slots["0:57"].Count != 2 || state.Taken != 0 {
|
|
t.Fatalf("claims success=%d bag=%+v state=%+v", success, bag, state)
|
|
}
|
|
}
|
|
|
|
func TestWeddingCapsuleFailureRollbackAndRelationshipReset(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, mode := range []string{"make_limit", "give_limit", "full_bag", "rollback", "bad_pair", "deleted", "wrong_account"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
repos, cat, rules, cmd := capsuleTestPair(t)
|
|
a, _, _ := repos.Character.Load(ctx, "1")
|
|
b, _, _ := repos.Character.Load(ctx, "2")
|
|
storeCapsule(&a, CapsuleState{Progress: 10, Own: 2, Taken: 2})
|
|
switch mode {
|
|
case "make_limit":
|
|
storeCapsule(&a, CapsuleState{Progress: 10, Own: 10})
|
|
case "give_limit":
|
|
cmd.Opcode = 1053
|
|
storeCapsule(&b, CapsuleState{Taken: 9})
|
|
case "full_bag":
|
|
cmd.Opcode = 1054
|
|
cmd.Grant = func(*repo.InventoryRecord, Item) error { return errors.New("full") }
|
|
case "rollback":
|
|
cmd.Opcode = 1054
|
|
repos.CharacterTrade = failingWeddingTrade{repos.CharacterTrade}
|
|
case "bad_pair":
|
|
b.Stats["wedding_partner_id"] = 3
|
|
case "deleted":
|
|
b.Stats["delete_flag"] = 1
|
|
case "wrong_account":
|
|
cmd.AccountID = "wrong"
|
|
}
|
|
if err := repos.Character.Save(ctx, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Character.Save(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bag, _, _ := repos.Inventory.Load(ctx, "1")
|
|
if _, err := CommitCapsule(ctx, repos, cat, rules, cmd); err == nil {
|
|
t.Fatal("invalid operation succeeded")
|
|
}
|
|
afterA, _, _ := repos.Character.Load(ctx, "1")
|
|
afterB, _, _ := repos.Character.Load(ctx, "2")
|
|
afterBag, _, _ := repos.Inventory.Load(ctx, "1")
|
|
if !reflect.DeepEqual(a, afterA) || !reflect.DeepEqual(b, afterB) || !reflect.DeepEqual(bag, afterBag) {
|
|
t.Fatal("failure changed assets")
|
|
}
|
|
})
|
|
}
|
|
repos, cat, rules, cmd := capsuleTestPair(t)
|
|
a, _, _ := repos.Character.Load(ctx, "1")
|
|
storeCapsule(&a, CapsuleState{10, 2, 3})
|
|
if err := repos.Character.Save(ctx, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := Commit(ctx, repos, cat, Command{ActorID: "1", PartnerID: "2", AccountID: "a1", Operation: 7, Now: cmd.Now})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range res.Characters {
|
|
if state, err := ReadCapsule(c, rules); err != nil || state != (CapsuleState{}) || c.Stats[capsulePrefix+"pair"] != 0 {
|
|
t.Fatalf("divorce retained capsule: %+v %v", state, err)
|
|
}
|
|
}
|
|
// Even an imported stale row is scoped to its marriage date and pair.
|
|
a.Stats["wedding_date"]++
|
|
if state, err := ReadCapsule(a, rules); err != nil || state != (CapsuleState{}) {
|
|
t.Fatalf("old marriage reused %+v %v", state, err)
|
|
}
|
|
}
|
|
|
|
func TestWeddingRingPurchaseAtomicCurrencies(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, mode := range []string{"success", "gold", "love", "mismatch", "full_bag", "rollback", "not_married"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
repos, cat, _, capsule := capsuleTestPair(t)
|
|
cmd := RingPurchase{ActorID: "1", PartnerID: "2", AccountID: "a1", Item: Item{10002613, 1}, Gold: 3500000, Love: 500, Now: capsule.Now, Grant: capsule.Grant}
|
|
a, _, _ := repos.Character.Load(ctx, "1")
|
|
b, _, _ := repos.Character.Load(ctx, "2")
|
|
switch mode {
|
|
case "gold":
|
|
a.Stats["gold"] = 3499999
|
|
case "love":
|
|
a.Stats["wedding_love"] = 499
|
|
b.Stats["wedding_love"] = 499
|
|
case "mismatch":
|
|
b.Stats["wedding_love"] = 945
|
|
case "full_bag":
|
|
cmd.Grant = func(*repo.InventoryRecord, Item) error { return errors.New("full") }
|
|
case "rollback":
|
|
repos.CharacterTrade = failingWeddingTrade{repos.CharacterTrade}
|
|
case "not_married":
|
|
a.Stats["wedding_state"] = 1
|
|
b.Stats["wedding_state"] = 1
|
|
}
|
|
if err := repos.Character.Save(ctx, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Character.Save(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bag, _, _ := repos.Inventory.Load(ctx, "1")
|
|
res, err := BuyRingTicket(ctx, repos, cat, cmd)
|
|
if mode == "success" {
|
|
if err != nil || res.Characters[0].Stats["gold"] != 5500000 || res.Characters[1].Stats["gold"] != 9000000 || res.Characters[0].Stats["wedding_love"] != 446 || res.Characters[1].Stats["wedding_love"] != 446 || res.Inventories[0].Slots["0:57"].Count != 1 || res.Characters[0].Stats["wedding_ring"] != 2 || res.Characters[1].Stats["wedding_ring"] != 2 {
|
|
t.Fatalf("purchase=%+v %v", res, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("invalid purchase succeeded")
|
|
}
|
|
afterA, _, _ := repos.Character.Load(ctx, "1")
|
|
afterB, _, _ := repos.Character.Load(ctx, "2")
|
|
afterBag, _, _ := repos.Inventory.Load(ctx, "1")
|
|
if !reflect.DeepEqual(a, afterA) || !reflect.DeepEqual(b, afterB) || !reflect.DeepEqual(bag, afterBag) {
|
|
t.Fatal("failed purchase changed assets")
|
|
}
|
|
})
|
|
}
|
|
}
|