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

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

246 lines
9.3 KiB
Go

package worldmap
import (
"errors"
"testing"
)
func TestParseTournamentPVFShape(t *testing.T) {
dungeonDoc := parseTestDocument(t, "dungeon/tournament.dgn", `
[basis level]
72
[maze info]
[size]
1 1
[greed]
`+"`BB`"+`
[start map]
0 0
[boss map]
0 0
[tournament dungeon]
1
[required item]
4726 1 1
[/required item]
[tournament round fatigue]
2
[result card]
[reward item rate]
3329 60 1 3329 35 2 3329 5 3
[/reward item rate]
0 0 0 100 1 70 0 30 2 80 0 20 3 90 0 10 4 100 0 0 5 0 100 0
[/result card]
[tournament clear reward exp]
0 0 1 30000 2 50000 3 100000 4 200000
[/tournament clear reward exp]
[limit party count]
1
[tournament clear reward gold rate]
16
`)
dungeon := ParseDungeon(120, "dungeon/tournament.dgn", dungeonDoc)
if !dungeon.Metadata.Flags["tournament dungeon"] || dungeon.Metadata.TournamentRoundFatigue != 2 || dungeon.Metadata.TournamentGoldRate != 16 {
t.Fatalf("tournament scalar metadata=%+v", dungeon.Metadata)
}
if len(dungeon.Metadata.RequiredItems) != 1 || dungeon.Metadata.RequiredItems[0] != (TicketItem{ItemID: 4726, Count: 1}) {
t.Fatalf("required items=%+v", dungeon.Metadata.RequiredItems)
}
if len(dungeon.Metadata.TournamentRewardItemRates) != 3 || dungeon.Metadata.TournamentRewardItemRates[2].Count != 3 {
t.Fatalf("reward item rates=%+v", dungeon.Metadata.TournamentRewardItemRates)
}
if len(dungeon.Metadata.TournamentResultCards) != 6 || dungeon.Metadata.TournamentResultCards[5].ItemWeight != 100 {
t.Fatalf("result cards=%+v", dungeon.Metadata.TournamentResultCards)
}
if len(dungeon.Metadata.TournamentClearRewardExp) != 5 || dungeon.Metadata.TournamentClearRewardExp[4].Experience != 200000 {
t.Fatalf("clear reward experience=%+v", dungeon.Metadata.TournamentClearRewardExp)
}
mapDoc := parseTestDocument(t, "map/tournament.map", `
[dungeon]
120
[/dungeon]
[tournament start area]
1 360 270 1
[tournament start area]
1 760 270 2
[tournament enemies]
1 `+"`[monster]`"+` 56048 900 `+"`A`"+` 56047 300 `+"`B`"+`
[/tournament enemies]
`)
parsedMap := ParseMap(24001, "map/tournament.map", mapDoc)
if parsedMap.TournamentMalformed || len(parsedMap.TournamentStartAreas) != 2 || len(parsedMap.TournamentEnemies) != 2 {
t.Fatalf("tournament map=%+v", parsedMap)
}
if parsedMap.TournamentEnemies[0].Kind != "[monster]" || parsedMap.TournamentEnemies[1].Strength != 300 {
t.Fatalf("tournament enemies=%+v", parsedMap.TournamentEnemies)
}
}
func TestTournamentRuntimeBuildsBracketAndRejectsStaleDeaths(t *testing.T) {
definition := tournamentRuntimeTestDefinition()
runtime, err := NewTournamentRuntime(definition, func(maximum int) (int, error) { return 0, nil })
if err != nil {
t.Fatal(err)
}
if len(runtime.Rounds) != 4 || len(runtime.Rounds[0].Teams) != 16 || len(runtime.Rounds[3].Teams) != 2 || len(runtime.PathActors) != 4 {
t.Fatalf("rounds=%+v path=%+v", runtime.Rounds, runtime.PathActors)
}
if !runtime.BindFirstActorSequence(100) || !runtime.BindFirstActorSequence(100) || runtime.BindFirstActorSequence(101) {
t.Fatal("first actor sequence binding is not idempotent")
}
if got := runtime.ApplyActorDeath("future", 101); got.Disposition != TournamentDeathRejected {
t.Fatalf("future death=%+v", got)
}
for round := byte(1); round <= 4; round++ {
sequence := uint16(99 + round)
transition := runtime.ApplyActorDeath(string([]byte{'e', '0' + round}), sequence)
want := TournamentRoundCompleted
if round == 4 {
want = TournamentCompleted
}
if transition.Disposition != want || transition.CompletedRound != round {
t.Fatalf("round=%d transition=%+v want=%d", round, transition, want)
}
if duplicate := runtime.ApplyActorDeath(string([]byte{'e', '0' + round}), sequence); duplicate.Disposition != TournamentDeathDuplicate {
t.Fatalf("round=%d duplicate=%+v", round, duplicate)
}
}
current, completed, terminal := runtime.Snapshot()
if current != 4 || completed != 4 || terminal != TournamentChampion {
t.Fatalf("terminal snapshot=%d/%d/%d", current, completed, terminal)
}
}
func TestTournamentRuntimeRejectsInvalidRandomIndex(t *testing.T) {
_, err := NewTournamentRuntime(tournamentRuntimeTestDefinition(), func(maximum int) (int, error) { return maximum, nil })
if !errors.Is(err, ErrTournamentRandomInvalid) {
t.Fatalf("error=%v", err)
}
}
func TestTournamentRuntimeEliminationIsTerminalAndIdempotent(t *testing.T) {
runtime, err := NewTournamentRuntime(tournamentRuntimeTestDefinition(), func(maximum int) (int, error) { return 0, nil })
if err != nil {
t.Fatal(err)
}
if !runtime.BindFirstActorSequence(100) {
t.Fatal("bind sequence")
}
if !runtime.Eliminate() || runtime.Eliminate() {
t.Fatal("elimination was not a one-way transition")
}
current, completed, terminal := runtime.Snapshot()
if current != 1 || completed != 0 || terminal != TournamentEliminated {
t.Fatalf("eliminated snapshot=%d/%d/%d", current, completed, terminal)
}
if transition := runtime.ApplyActorDeath("after-elimination", 100); transition.Disposition != TournamentDeathRejected {
t.Fatalf("post-elimination death=%+v", transition)
}
}
func tournamentRuntimeTestDefinition() TournamentDefinition {
candidates := make([]TournamentActor, 15)
for index := range candidates {
candidates[index] = TournamentActor{Code: uint32(56000 + index), Strength: uint16(100 + index), Name: "actor", Level: 72}
}
return TournamentDefinition{DungeonID: 120, MapID: 24001, DifficultyLevel: 72, PartyCount: 1, Candidates: candidates}
}
func TestTournamentRewardPlanUsesUpperCompletedRoundsAndChampionLowerKey(t *testing.T) {
definition := TournamentDefinition{
PartyCount: 1,
ResultCards: map[byte]TournamentResultCard{
0: {ResultKey: 0, EmptyWeight: 1}, 1: {ResultKey: 1, EmptyWeight: 1},
2: {ResultKey: 2, EmptyWeight: 1}, 3: {ResultKey: 3, EmptyWeight: 1},
4: {ResultKey: 4, GoldWeight: 1}, 5: {ResultKey: 5, ItemWeight: 1},
},
RewardItems: []TournamentRewardItemRate{{ItemID: 3329, Weight: 1, Count: 3}},
}
runtime := &TournamentRuntime{Definition: definition, terminal: TournamentChampion, completedRounds: tournamentRoundCount}
plan, created, err := runtime.PrepareRewardPlan(200000, 777, func(maximum int) (int, error) { return 0, nil })
if err != nil || !created {
t.Fatalf("prepare plan created=%t err=%v", created, err)
}
for index, reward := range plan.Upper {
if !reward.Gold || reward.Count != 777 || reward.Empty {
t.Fatalf("upper[%d]=%+v, want frozen gold", index, reward)
}
}
for index, reward := range plan.Lower {
if reward.ItemID != 3329 || reward.Count != 3 || reward.Gold || reward.Empty {
t.Fatalf("lower[%d]=%+v, want item", index, reward)
}
}
second, created, err := runtime.PrepareRewardPlan(1, 2, func(maximum int) (int, error) { return 0, nil })
if err != nil || created || second.Experience != 200000 || second.Upper[0].Count != 777 {
t.Fatalf("second prepare created=%t plan=%+v err=%v", created, second, err)
}
}
func TestTournamentRewardPlanSelectionIsTwoPhaseAndIdempotent(t *testing.T) {
plan := TournamentRewardPlan{Champion: false, Upper: [2]TournamentReward{{ItemID: 3329, Count: 3}}}
plan.initSelection()
reward, reserved, replay := plan.ReserveSelection(0, 0)
if !reserved || replay || reward.ItemID != 3329 {
t.Fatalf("reserve=%+v reserved=%t replay=%t", reward, reserved, replay)
}
if _, second, _ := plan.ReserveSelection(0, 1); second {
t.Fatal("second card reservation unexpectedly accepted")
}
if !plan.CommitSelection(0, 0) || !plan.IsSelectionComplete() {
t.Fatal("committed non-champion selection is not complete")
}
replayReward, replayReserved, replay := plan.ReserveSelection(0, 0)
if replayReserved || !replay || replayReward.ItemID != 3329 {
t.Fatalf("replay=%+v reserved=%t replay=%t", replayReward, replayReserved, replay)
}
if _, differentReserved, differentReplay := plan.ReserveSelection(0, 1); differentReserved || differentReplay {
t.Fatalf("different card was accepted after commit: reserved=%t replay=%t", differentReserved, differentReplay)
}
}
func TestTournamentRewardPlanSelectedCardOnlyReportsCommittedSelection(t *testing.T) {
plan := TournamentRewardPlan{Champion: true}
plan.initSelection()
if _, ok := plan.SelectedCard(0); ok {
t.Fatal("unselected card was projected")
}
if _, reserved, _ := plan.ReserveSelection(0, 1); !reserved {
t.Fatal("selection was not reserved")
}
if _, ok := plan.SelectedCard(0); ok {
t.Fatal("uncommitted card was projected")
}
if !plan.CommitSelection(0, 1) {
t.Fatal("selection was not committed")
}
if index, ok := plan.SelectedCard(0); !ok || index != 1 {
t.Fatalf("selected card index=%d ok=%t, want 1/true", index, ok)
}
}
func TestTournamentCardlessRewardPlanCommitsWithoutSelectionRequest(t *testing.T) {
runtime := &TournamentRuntime{
Definition: TournamentDefinition{ResultCards: map[byte]TournamentResultCard{
4: {ResultKey: 4, EmptyWeight: 1},
5: {ResultKey: 5, EmptyWeight: 1},
}},
terminal: TournamentChampion,
completedRounds: tournamentRoundCount,
}
plan, _, err := runtime.PrepareRewardPlan(123, 456, func(maximum int) (int, error) { return 0, nil })
if err != nil {
t.Fatal(err)
}
if !plan.Cardless() || runtime.RewardSelectionComplete() {
t.Fatalf("initial cardless plan=%+v complete=%t", plan, runtime.RewardSelectionComplete())
}
if !runtime.CommitCardlessRewardPlan() || !runtime.RewardSelectionComplete() {
t.Fatal("cardless reward plan did not cross the delivery gate")
}
if !runtime.CommitCardlessRewardPlan() {
t.Fatal("cardless reward completion replay was not idempotent")
}
}