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,220 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user