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,284 @@
|
||||
package wedding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
modulepvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
"longheng.io/server/internal/modules/dnf/quest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestWeddingCeremonyAssetsAndReplay(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Unix(1788854400, 0)
|
||||
cat := &Catalog{MinimumLevel: 30, RemarryDays: 1, WeddingDays: 1, Alimony: [2]int64{500000, 500000}, MaxLove: 999, LoveItems: map[int64]int64{10002901: 1}, QuestIDs: [2]int64{20737, 20738}, Delays: [3]int64{300, 600, 900}, EndSeconds: 60, PaidType: 2, PaidTicket: Item{2683401, 1}}
|
||||
cat.Ceremonies[0] = Ceremony{Gold: 500000, Ring: 0, Gift: Item{10002629, 1}}
|
||||
cat.Ceremonies[2] = Ceremony{Ring: 2, Gift: Item{10002631, 1}}
|
||||
cat.RingTickets[1] = Item{10002611, 1}
|
||||
for _, mode := range []string{"complete", "missing_quest", "gold", "full_bag", "paid_ticket_missing", "paid", "rollback"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
repos := memory.NewMemoryGroup()
|
||||
for _, id := range []string{"1", "2"} {
|
||||
job := "0"
|
||||
if id == "2" {
|
||||
job = "1"
|
||||
}
|
||||
gold := int64(600000)
|
||||
if mode == "gold" {
|
||||
gold = 499999
|
||||
}
|
||||
if err := repos.Character.Save(ctx, repo.CharacterRecord{CharacterID: id, AccountID: "a" + id, Job: job, Level: 30, Stats: map[string]int64{"gold": gold}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, repo.InventoryRecord{CharacterID: id, Slots: map[string]repo.ItemStack{"0:3": {ItemID: 2683401, Count: 1}, "0:4": {ItemID: 10002611, Count: 1}, "0:5": {ItemID: 10002901, Count: 2, RawEntry: make([]byte, 119), Extra: map[string]string{"quantity": "2"}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := repo.QuestRecord{CharacterID: id, States: map[int64]repo.QuestState{20737: {Status: "completed"}, 20738: {Status: "completed"}}}
|
||||
if mode == "missing_quest" && id == "2" {
|
||||
delete(q.States, 20738)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, q); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cmd := Command{ActorID: "1", PartnerID: "2", AccountID: "a1", PartnerAccountID: "a2", Operation: 1, Now: now}
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Operation, cmd.Room = 2, 8
|
||||
cmd.Grant = func(bag *repo.InventoryRecord, item Item) error {
|
||||
if mode == "full_bag" && bag.CharacterID == "2" {
|
||||
return errors.New("bag full")
|
||||
}
|
||||
bag.Slots["0:6"] = repo.ItemStack{ItemID: item.ID, Count: item.Count}
|
||||
return nil
|
||||
}
|
||||
if mode == "paid" || mode == "paid_ticket_missing" {
|
||||
cmd.Kind = 2
|
||||
}
|
||||
if mode == "paid_ticket_missing" {
|
||||
bag, _, _ := repos.Inventory.Load(ctx, "1")
|
||||
delete(bag.Slots, "0:3")
|
||||
if err := repos.Inventory.Save(ctx, bag); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
beforeC, _, _ := repos.Character.Load(ctx, "1")
|
||||
beforeB, _, _ := repos.Inventory.Load(ctx, "1")
|
||||
if mode == "rollback" {
|
||||
repos.CharacterTrade = failingWeddingTrade{repos.CharacterTrade}
|
||||
}
|
||||
result, err := Commit(ctx, repos, cat, cmd)
|
||||
if mode != "complete" && mode != "paid" {
|
||||
if err == nil {
|
||||
t.Fatal("invalid reservation committed")
|
||||
}
|
||||
a, _, _ := repos.Character.Load(ctx, "1")
|
||||
bag, _, _ := repos.Inventory.Load(ctx, "1")
|
||||
if !reflect.DeepEqual(a, beforeC) || !reflect.DeepEqual(bag, beforeB) {
|
||||
t.Fatal("failed reservation charged assets")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantGold := int64(100000)
|
||||
if mode == "paid" {
|
||||
wantGold = 600000
|
||||
if _, ok := result.Inventories[0].Slots["0:3"]; ok {
|
||||
t.Fatal("ticket not consumed")
|
||||
}
|
||||
}
|
||||
if result.Characters[0].Stats["gold"] != wantGold || result.Characters[1].Stats["gold"] != 600000 {
|
||||
t.Fatal("wrong payment")
|
||||
}
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("reservation replay charged again")
|
||||
}
|
||||
cmd.Operation = 3
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("early ceremony")
|
||||
}
|
||||
cmd.Now = now.Add(300 * time.Second)
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Operation = 4
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("early completion")
|
||||
}
|
||||
cmd.Now = now.Add(360 * time.Second)
|
||||
result, err = Commit(ctx, repos, cat, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, c := range result.Characters {
|
||||
state, e := Read(c)
|
||||
if e != nil || state.Status != 4 || state.Room != 0 || state.Date != cmd.Now.Unix() || result.Inventories[i].Slots["0:6"].ItemID != cat.Ceremonies[cmd.Kind].Gift.ID {
|
||||
t.Fatalf("completion: %+v %v", result, e)
|
||||
}
|
||||
}
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("completion replay granted again")
|
||||
}
|
||||
if mode == "paid" {
|
||||
return
|
||||
}
|
||||
cmd.Operation, cmd.Slot, cmd.Ring = 8, 4, 1
|
||||
result, err = Commit(ctx, repos, cat, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Characters[1].Stats["wedding_ring"] != 1 {
|
||||
t.Fatal("partner ring stale")
|
||||
}
|
||||
cmd.Operation, cmd.Slot = 9, 5
|
||||
result, err = Commit(ctx, repos, cat, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stack := result.Inventories[0].Slots["0:5"]
|
||||
if stack.Count != 1 || stack.Extra["quantity"] != "1" || binary.LittleEndian.Uint32(stack.RawEntry[6:10]) != 1 || result.Characters[1].Stats["wedding_love"] != 1 {
|
||||
t.Fatal("love item projection mismatch")
|
||||
}
|
||||
cat.MaxLove = 1
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("overflow consumed love item")
|
||||
}
|
||||
cat.MaxLove = 999
|
||||
cmd.Operation = 7
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("divorce without sufficient gold")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type failingWeddingTrade struct{ repo.CharacterTradeUnitOfWork }
|
||||
|
||||
func (u failingWeddingTrade) WithinCharacterTrade(ctx context.Context, a, b string, apply func(repo.CharacterRepository, repo.InventoryRepository) error) error {
|
||||
return u.CharacterTradeUnitOfWork.WithinCharacterTrade(ctx, a, b, func(chars repo.CharacterRepository, bags repo.InventoryRepository) error {
|
||||
if err := apply(chars, bags); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("injected commit failure")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWeddingReciprocalTransaction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Unix(1788854400, 0)
|
||||
cat := &Catalog{MinimumLevel: 30, RemarryDays: 1, Alimony: [2]int64{500000, 500000}, MaxLove: 999, LoveItems: map[int64]int64{10002901: 1}}
|
||||
cat.RingTickets[1] = Item{10002611, 1}
|
||||
for _, failure := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "commit", true: "rollback"}[failure], func(t *testing.T) {
|
||||
repos := memory.NewMemoryGroup()
|
||||
initial := []repo.CharacterRecord{
|
||||
{CharacterID: "1", AccountID: "dnf:1", Job: "0", Level: 30, Stats: map[string]int64{"gold": 600000}},
|
||||
{CharacterID: "2", AccountID: "dnf:2", Job: "1", Level: 30, Stats: map[string]int64{"gold": 600000}},
|
||||
}
|
||||
for _, c := range initial {
|
||||
if err := repos.Character.Save(ctx, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if failure {
|
||||
repos.CharacterTrade = failingWeddingTrade{repos.CharacterTrade}
|
||||
}
|
||||
cmd := Command{ActorID: "1", PartnerID: "2", AccountID: "dnf:1", PartnerAccountID: "dnf:2", Operation: 1, Now: now}
|
||||
result, err := Commit(ctx, repos, cat, cmd)
|
||||
if failure {
|
||||
if err == nil {
|
||||
t.Fatal("injected failure accepted")
|
||||
}
|
||||
for _, want := range initial {
|
||||
got, _, err := repos.Character.Load(ctx, want.CharacterID)
|
||||
if err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("partial write: %+v %v", got, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, _ := Read(result.Characters[0])
|
||||
b, _ := Read(result.Characters[1])
|
||||
if a.Status != 1 || b.Status != 1 || a.PartnerID != 2 || b.PartnerID != 1 || a.PairID != b.PairID {
|
||||
t.Fatalf("inconsistent pair: %+v %+v", a, b)
|
||||
}
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err == nil {
|
||||
t.Fatal("replayed engagement committed")
|
||||
}
|
||||
cmd.Operation = 5
|
||||
if _, err := Commit(ctx, repos, cat, cmd); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range initial {
|
||||
got, _, _ := repos.Character.Load(ctx, c.CharacterID)
|
||||
state, e := Read(got)
|
||||
if e != nil || state.Status != 0 || state.PartnerID != 0 || got.Stats["gold"] != 600000 {
|
||||
t.Fatalf("breakup: %+v %v", got, e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, alter := range []func(*repo.CharacterRecord){
|
||||
func(c *repo.CharacterRecord) { c.Level = 29 },
|
||||
func(c *repo.CharacterRecord) { c.Job = "0" },
|
||||
func(c *repo.CharacterRecord) { c.CharacterID = "1" },
|
||||
func(c *repo.CharacterRecord) { c.Stats = map[string]int64{"delete_flag": 1} },
|
||||
func(c *repo.CharacterRecord) { c.Stats = map[string]int64{"wedding_divorced_at": now.Unix() - 60} },
|
||||
} {
|
||||
a := repo.CharacterRecord{CharacterID: "1", Job: "0", Level: 30}
|
||||
b := repo.CharacterRecord{CharacterID: "2", Job: "1", Level: 30}
|
||||
alter(&b)
|
||||
if err := Validate(a, b, cat, 1, now); err == nil {
|
||||
t.Fatalf("invalid proposal accepted: %+v", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeddingRuntimePVF(t *testing.T) {
|
||||
path := os.Getenv("DNF90_PVF_PATH")
|
||||
if path == "" {
|
||||
t.Skip("set DNF90_PVF_PATH for runtime wedding catalog validation")
|
||||
}
|
||||
archive, err := platformpvf.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := LoadCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.MinimumLevel != 30 || c.MaxLove != 999 || c.Alimony != [2]int64{500000, 500000} || c.Delays != [3]int64{300, 600, 900} || c.PaidTicket != (Item{2683401, 1}) || c.Ceremonies[0].Gold != 500000 || c.Ceremonies[1].Gold != 2000000 || c.RingTickets[5].ID != 10002615 || c.LoveItems[10002901] != 1 {
|
||||
t.Fatalf("catalog mismatch: %+v", c)
|
||||
}
|
||||
index, err := modulepvf.Build(context.Background(), archive, modulepvf.BuildOptions{Lists: []string{quest.DefaultList}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
quests, err := quest.Load(context.Background(), index)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
eligibility := quest.CharacterEligibility{Level: 30, Job: 0, Wedding: true}
|
||||
first, err := quests.PlanAccept(eligibility, repo.QuestRecord{CharacterID: "1"}, 20737)
|
||||
if err != nil || first.InitTrigger != 5 {
|
||||
t.Fatalf("real wedding quest 1: %+v %v", first, err)
|
||||
}
|
||||
second, err := quests.PlanAccept(eligibility, repo.QuestRecord{CharacterID: "1", States: map[int64]repo.QuestState{20737: {Status: "completed"}}}, 20738)
|
||||
if err != nil || second.QuestID != 20738 {
|
||||
t.Fatalf("real wedding quest 2: %+v %v", second, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user