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,250 @@
|
||||
package pvp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
func totalProgressTestCatalog(t *testing.T) *Catalog {
|
||||
t.Helper()
|
||||
s := dailyTestSource()
|
||||
s["etc/missionsystem.etc"] = strings.ReplaceAll(s["etc/missionsystem.etc"], "casual daily mission", "total match daily mission")
|
||||
s["etc/pvp_ref.etc"] = strings.ReplaceAll(s["etc/pvp_ref.etc"], "pvp grade point", "pvp grade point total match")
|
||||
s["etc/pvp_ref.etc"] = strings.ReplaceAll(s["etc/pvp_ref.etc"], "1000", "1500")
|
||||
for i, condition := range []string{"hereafter join", "kill count", "total match win all kill"} {
|
||||
s[fmt.Sprintf("pvp_mission/daily/%c.msn", 'a'+i)] = fmt.Sprintf("[grade] `[total match daily]` %d\n[rank range] 0 30\n[condition] `[%s]` `[total match mode]` %d 1\n[/condition]\n[reward victory emblem] 80\n", i, condition, []int{3, 9, 1}[i])
|
||||
}
|
||||
for _, d := range []struct {
|
||||
id int
|
||||
category, condition string
|
||||
target, pre int
|
||||
}{
|
||||
{800, "battle", "hereafter join", 100, -1},
|
||||
{801, "battle", "hereafter join", 333, 800},
|
||||
{824, "victory", "hereafter winning count", 100, -1},
|
||||
} {
|
||||
p := fmt.Sprintf("hidden/%d.msn", d.id)
|
||||
s["pvp_mission/mission.lst"] += fmt.Sprintf("%d `%s`\n", d.id, p)
|
||||
s["pvp_mission/"+p] = fmt.Sprintf("[grade] `[total match normal]`\n[normal category] `[%s]`\n[rank range] 0 30\n[condition] `[%s]` `[total match mode]` %d -1\n[/condition]\n[pre required mission] %d\n", d.category, d.condition, d.target, d.pre)
|
||||
}
|
||||
s["pvp_mission/mission.lst"] += "499 `hidden/ordinary.msn`\n"
|
||||
s["pvp_mission/hidden/ordinary.msn"] = "[grade] `[normal]`\n"
|
||||
c, err := LoadTotal(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.Normal) != 3 || c.Normal[1].PreRequired != 800 || c.RatingRules.GradePoints[0][1] != 1500 {
|
||||
t.Fatal("mixed ordinary and total catalog")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type totalFailCommit struct{ repo.RentalAssetUnitOfWork }
|
||||
|
||||
func (u totalFailCommit) WithinPVPTotal(ctx context.Context, a string, ids [3]string, apply func(repo.AccountRepository, repo.CharacterRepository) error) error {
|
||||
return u.RentalAssetUnitOfWork.(repo.PVPTotalUnitOfWork).WithinPVPTotal(ctx, a, ids, func(a repo.AccountRepository, c repo.CharacterRepository) error {
|
||||
if err := apply(a, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("injected total commit failure")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTotalProgressAtomicReplaySeparateRankAndDayReset(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
||||
c := totalProgressTestCatalog(t)
|
||||
repos := memory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, repo.AccountRecord{AccountID: "a", Metadata: map[string]string{"keep": "value"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := [3]string{"1", "2", "3"}
|
||||
for _, id := range ids {
|
||||
if err := repos.Character.Save(ctx, repo.CharacterRecord{CharacterID: id, AccountID: "a", Stats: map[string]int64{ExperienceKey: 11000, RankPointKey: 1234, WinsKey: 7, "pvp_daily_day": 991, EmblemsKey: 10}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
before, err := PrepareTotal(ctx, repos, c, "a", "1", ids, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
match := TotalMatchFacts{Match: Match{Key: "battle1", StartedAt: now, CompletedAt: now.Add(time.Minute), Won: true}, Kills: [3]uint32{0, 3, 0}, Remaining: 3}
|
||||
bad := repos
|
||||
bad.RentalAssets = totalFailCommit{repos.RentalAssets}
|
||||
if _, err := CompleteTotal(ctx, bad, c, "a", "1", ids, match); err == nil {
|
||||
t.Fatal("failed commit succeeded")
|
||||
}
|
||||
for _, id := range ids {
|
||||
v, _, _ := repos.Character.Load(ctx, id)
|
||||
if v.Stats[ExperienceKey] != 11000 || v.Stats[EmblemsKey] != 10 {
|
||||
t.Fatal("partial reward escaped rollback")
|
||||
}
|
||||
}
|
||||
var first TotalSnapshot
|
||||
for i := 0; i < 2; i++ {
|
||||
after, err := CompleteTotal(ctx, repos, c, "a", "1", ids, match)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if i == 0 {
|
||||
first = after
|
||||
} else if !reflect.DeepEqual(first.Stats, after.Stats) || !reflect.DeepEqual(first.ChangedRows, after.ChangedRows) || after.Before[0].Stats[ExperienceKey] != 11000 {
|
||||
t.Fatal("receipt did not restore identical result")
|
||||
}
|
||||
if after.Stats[CertificationKey] != 1 || after.Stats[WinsKey] != 1 || after.Stats[RankPointKey] != 1050 || after.Rows[0].Progress != 1 || after.Rows[1].Progress != 3 || after.Rows[2].Progress != 1 {
|
||||
t.Fatal("total facts not persisted", after.Stats, after.Rows)
|
||||
}
|
||||
if !reflect.DeepEqual(after.NormalRows, []Row{{800, 1, 100}, {801, 1, 333}, {824, 1, 100}}) ||
|
||||
!reflect.DeepEqual(after.BeforeNormalRows, []Row{{800, 0, 100}, {801, 0, 333}, {824, 0, 100}}) || len(after.ChangedRows) != 6 {
|
||||
t.Fatal("total target counters or frozen result deltas missing", after.NormalRows, after.ChangedRows)
|
||||
}
|
||||
for j, v := range after.Characters {
|
||||
if v.Stats[ExperienceKey] != 12500 || v.Stats[GradeKey] != 1 || v.Stats[RankPointKey] != 1234 || v.Stats[WinsKey] != 7 || v.Stats["pvp_daily_day"] != 991 {
|
||||
t.Fatal("actor experience or personal counters corrupted", v.Stats)
|
||||
}
|
||||
want := int64(10)
|
||||
if j == 0 {
|
||||
want += 80
|
||||
}
|
||||
if v.Stats[EmblemsKey] != want {
|
||||
t.Fatal("daily reward duplicated or wrong owner", v.Stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
if before.Characters[0].Stats[ExperienceKey] != 11000 {
|
||||
t.Fatal("input mutated")
|
||||
}
|
||||
a, _, _ := repos.Account.Load(ctx, "a")
|
||||
if a.Metadata["keep"] != "value" {
|
||||
t.Fatal("unrelated account metadata lost")
|
||||
}
|
||||
reset, err := PrepareTotal(ctx, repos, c, "a", "2", ids, now.Add(24*time.Hour))
|
||||
if err != nil || reset.Rows[0].Progress != 0 || reset.Stats[RankPointKey] != 1050 || !reflect.DeepEqual(reset.NormalRows, first.NormalRows) {
|
||||
t.Fatal("day reset lost rating", err)
|
||||
}
|
||||
if _, err := CompleteTotal(ctx, repos, c, "a", "1", [3]string{"1", "2", "4"}, match); err == nil {
|
||||
t.Fatal("receipt reused for another roster")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalMissionsWithoutRosterShareAccountAndRollBack(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
||||
c := totalProgressTestCatalog(t)
|
||||
repos := memory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, repo.AccountRecord{AccountID: "a", Metadata: map[string]string{"keep": "value"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range []string{"1", "2"} {
|
||||
if err := repos.Character.Save(ctx, repo.CharacterRecord{AccountID: "a", CharacterID: id, Stats: map[string]int64{WinsKey: 99, CertificationKey: 123}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
bad := repos
|
||||
bad.RentalAssets = renameFailCommit{repos.RentalAssets}
|
||||
if _, err := PrepareTotalMissions(ctx, bad, c, "a", "1", now); err == nil {
|
||||
t.Fatal("commit failure ignored")
|
||||
}
|
||||
a, _, _ := repos.Account.Load(ctx, "a")
|
||||
if a.Metadata[TotalProgressMetadataKey] != "" {
|
||||
t.Fatal("mission initialization escaped rollback")
|
||||
}
|
||||
first, err := PrepareTotalMissions(ctx, repos, c, "a", "1", now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other, err := PrepareTotalMissions(ctx, repos, c, "a", "2", now)
|
||||
if err != nil || !reflect.DeepEqual(first.Rows, other.Rows) || !reflect.DeepEqual(first.NormalRows, other.NormalRows) {
|
||||
t.Fatal("missions changed with character", err)
|
||||
}
|
||||
if first.NormalRows[0].Progress != 0 || first.NormalRows[2].Progress != 0 {
|
||||
t.Fatal("ordinary counters became total progress")
|
||||
}
|
||||
if _, err := PrepareTotalMissions(ctx, repos, c, "foreign", "1", now); err == nil {
|
||||
t.Fatal("foreign owner accepted")
|
||||
}
|
||||
a, _, _ = repos.Account.Load(ctx, "a")
|
||||
if a.Metadata[TotalTeamMetadataKey] != "" || a.Metadata["keep"] != "value" {
|
||||
t.Fatal("initializing missions changed team or unrelated metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalFactsConditionsAndTransactionScope(t *testing.T) {
|
||||
f := TotalMatchFacts{Match: Match{Won: true}, Kills: [3]uint32{1, 2, 0}, Remaining: 2, ReportsCount: map[uint16]uint32{138: 9}}
|
||||
for _, row := range []struct {
|
||||
condition string
|
||||
param int64
|
||||
streak int64
|
||||
want uint32
|
||||
}{
|
||||
{"total match win left character", 2, 0, 1}, {"total match win left character", 3, 0, 0},
|
||||
{"total match win all kill", 1, 0, 0}, {"kill count", 1, 0, 3}, {"winning streak", 3, 3, 1}, {"winning streak", 3, 4, 0}, {"reduce hp", 30, 0, 5},
|
||||
} {
|
||||
if got := totalMissionAdvance(Mission{ID: 138, Condition: row.condition, Parameter: row.param}, f, row.streak); got != row.want {
|
||||
t.Fatal(row, got)
|
||||
}
|
||||
}
|
||||
repos := memory.NewMemoryGroup()
|
||||
ctx := context.Background()
|
||||
err := repos.RentalAssets.(repo.PVPTotalUnitOfWork).WithinPVPTotal(ctx, "a", [3]string{"1", "2", "3"}, func(a repo.AccountRepository, c repo.CharacterRepository) error {
|
||||
if _, _, err := a.Load(ctx, "b"); err == nil {
|
||||
t.Fatal("foreign account admitted")
|
||||
}
|
||||
if _, _, err := c.Load(ctx, "4"); err == nil {
|
||||
t.Fatal("foreign character admitted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type renameFailCommit struct{ repo.RentalAssetUnitOfWork }
|
||||
|
||||
func (u renameFailCommit) WithinRentalAssets(ctx context.Context, a, c string, apply func(repo.AccountRepository, repo.CharacterRepository, repo.InventoryRepository, repo.EquipmentRepository) error) error {
|
||||
return u.RentalAssetUnitOfWork.WithinRentalAssets(ctx, a, c, func(a repo.AccountRepository, c repo.CharacterRepository, i repo.InventoryRepository, e repo.EquipmentRepository) error {
|
||||
if err := apply(a, c, i, e); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("injected rename commit failure")
|
||||
})
|
||||
}
|
||||
func TestTotalRenameRollbackPreservesNameAndCard(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
repos := memory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, repo.AccountRecord{AccountID: "a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SaveTotalTeam(ctx, repos.Account, "a", TotalTeam{Name: "旧队名"}, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, repo.CharacterRecord{AccountID: "a", CharacterID: "1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, repo.InventoryRecord{CharacterID: "1", Slots: map[string]repo.ItemStack{"0:100": {ItemID: TotalTeamRenameCardID, Count: 1, Extra: map[string]string{"equipment_lock_state": "0"}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bad := repos
|
||||
bad.RentalAssets = renameFailCommit{repos.RentalAssets}
|
||||
if err := RenameTotalTeam(ctx, bad, "a", "1", "新队名", 100, now); err == nil {
|
||||
t.Fatal("failure not returned")
|
||||
}
|
||||
team, err := ReadTotalTeam(ctx, repos, "a")
|
||||
if err != nil || team.Team.Name != "旧队名" {
|
||||
t.Fatal(team, err)
|
||||
}
|
||||
inv, _, _ := repos.Inventory.Load(ctx, "1")
|
||||
if inv.Slots["0:100"].Count != 1 {
|
||||
t.Fatal("card escaped rollback")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user