DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
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")
}
}