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,151 @@
package guild
import (
"errors"
"testing"
)
type compoundTestSource string
func (s compoundTestSource) ReadText(string) (string, error) { return string(s), nil }
// 运行时 PVF 的原文。
const liveCompoundTable = `[compound info]
0 4 2000 1 4 1500 2 4 750 3 2 0
[/compound info]
[guild flag compound info]
0 4 2500 1 4 2000 2 4 1000 3 4 500 6 2 0
[/guid flag compound info]
`
func TestLoadCompoundCatalogMatchesTheLivePVF(t *testing.T) {
catalog, err := LoadCompoundCatalog(compoundTestSource(liveCompoundTable))
if err != nil {
t.Fatalf("LoadCompoundCatalog() error = %v", err)
}
for _, row := range []struct {
kind CompoundKind
rarity uint32
wantCount uint32
wantRate uint32
wantNext uint32
wantHasNext bool
}{
{kind: CompoundGem, rarity: 0, wantCount: 4, wantRate: 2000, wantNext: 1, wantHasNext: true},
{kind: CompoundGem, rarity: 1, wantCount: 4, wantRate: 1500, wantNext: 2, wantHasNext: true},
{kind: CompoundGem, rarity: 2, wantCount: 4, wantRate: 750, wantNext: 3, wantHasNext: true},
{kind: CompoundGem, rarity: 3, wantCount: 2, wantRate: 0},
{kind: CompoundFlag, rarity: 0, wantCount: 4, wantRate: 2500, wantNext: 1, wantHasNext: true},
{kind: CompoundFlag, rarity: 1, wantCount: 4, wantRate: 2000, wantNext: 2, wantHasNext: true},
{kind: CompoundFlag, rarity: 2, wantCount: 4, wantRate: 1000, wantNext: 3, wantHasNext: true},
// 勋章的 3 -> 6 这一跳必须靠"表里的下一行",不是 rarity+1:
// 稀有度 4、5 的勋章根本不存在。
{kind: CompoundFlag, rarity: 3, wantCount: 4, wantRate: 500, wantNext: 6, wantHasNext: true},
{kind: CompoundFlag, rarity: 6, wantCount: 2, wantRate: 0},
} {
ladder := catalog.Ladder(row.kind)
recipe, found := ladder.Recipe(row.rarity)
if !found {
t.Fatalf("%s rarity %d is missing from the ladder", row.kind, row.rarity)
}
if recipe.MaxMaterialCount != row.wantCount || recipe.RatePerMaterial != row.wantRate {
t.Fatalf("%s rarity %d = %+v, want max_count=%d rate_per_material=%d",
row.kind, row.rarity, recipe, row.wantCount, row.wantRate)
}
next, hasNext := ladder.NextRarity(row.rarity)
if hasNext != row.wantHasNext || (hasNext && next != row.wantNext) {
t.Fatalf("%s rarity %d next = %d, %t; want %d, %t",
row.kind, row.rarity, next, hasNext, row.wantNext, row.wantHasNext)
}
}
}
// 成功率 0 的那一行是封顶行,不是"永远失败"。
func TestCompoundTopRowIsNotCompoundable(t *testing.T) {
catalog, err := LoadCompoundCatalog(compoundTestSource(liveCompoundTable))
if err != nil {
t.Fatalf("LoadCompoundCatalog() error = %v", err)
}
for _, row := range []struct {
kind CompoundKind
rarity uint32
}{
{kind: CompoundGem, rarity: 3},
{kind: CompoundFlag, rarity: 6},
} {
recipe, _ := catalog.Ladder(row.kind).Recipe(row.rarity)
if recipe.Compoundable() {
t.Fatalf("%s rarity %d must be the top of the ladder", row.kind, row.rarity)
}
}
}
func TestLoadCompoundCatalogRejectsMalformedTables(t *testing.T) {
for name, table := range map[string]string{
"件数不成三元组": "[compound info]\n0 4\n[/compound info]\n[guild flag compound info]\n0 4 2500\n[/x]\n",
"稀有度不升序": "[compound info]\n1 4 2000 0 4 1500\n[/compound info]\n[guild flag compound info]\n0 4 2500\n[/x]\n",
"成功率超过分母": "[compound info]\n0 4 20000\n[/compound info]\n[guild flag compound info]\n0 4 2500\n[/x]\n",
} {
if _, err := LoadCompoundCatalog(compoundTestSource(table)); !errors.Is(err, ErrCompoundCatalogInvalid) {
t.Fatalf("%s: error = %v, want ErrCompoundCatalogInvalid", name, err)
}
}
if _, err := LoadCompoundCatalog(nil); !errors.Is(err, ErrCompoundCatalogUnavailable) {
t.Fatalf("nil source error = %v, want ErrCompoundCatalogUnavailable", err)
}
}
// 件数是 2..上限的区间,成功率随件数线性上涨、封顶 100%。
// 第一版把上限当成"必须正好这么多件",玩家放 2 件被一句「无法进行合成。」挡掉。
func TestCompoundSuccessRateScalesWithMaterialCount(t *testing.T) {
catalog, err := LoadCompoundCatalog(compoundTestSource(liveCompoundTable))
if err != nil {
t.Fatalf("LoadCompoundCatalog() error = %v", err)
}
for _, row := range []struct {
kind CompoundKind
rarity uint32
count uint32
want uint32
}{
// 勋章最低档满件数恰好 100%,是支持"第三列是每件贡献"这个读法的信号。
{kind: CompoundFlag, rarity: 0, count: 2, want: 5000},
{kind: CompoundFlag, rarity: 0, count: 3, want: 7500},
{kind: CompoundFlag, rarity: 0, count: 4, want: 10000},
{kind: CompoundFlag, rarity: 3, count: 2, want: 1000},
{kind: CompoundFlag, rarity: 3, count: 4, want: 2000},
{kind: CompoundGem, rarity: 0, count: 2, want: 4000},
{kind: CompoundGem, rarity: 0, count: 4, want: 8000},
{kind: CompoundGem, rarity: 2, count: 2, want: 1500},
{kind: CompoundGem, rarity: 2, count: 4, want: 3000},
// 区间外一律 0,且 AcceptsMaterialCount 会先把请求挡掉。
{kind: CompoundGem, rarity: 0, count: 1, want: 0},
{kind: CompoundGem, rarity: 0, count: 5, want: 0},
// 封顶行任何件数都不可合成。
{kind: CompoundGem, rarity: 3, count: 2, want: 0},
{kind: CompoundFlag, rarity: 6, count: 2, want: 0},
} {
recipe, found := catalog.Ladder(row.kind).Recipe(row.rarity)
if !found {
t.Fatalf("%s rarity %d missing", row.kind, row.rarity)
}
if got := recipe.SuccessRateFor(row.count); got != row.want {
t.Fatalf("%s rarity %d with %d materials = %d, want %d",
row.kind, row.rarity, row.count, got, row.want)
}
if accepted := recipe.AcceptsMaterialCount(row.count); accepted != (row.want > 0) {
t.Fatalf("%s rarity %d accepts %d = %t, want %t",
row.kind, row.rarity, row.count, accepted, row.want > 0)
}
}
}
// 成功率封顶 100%:哪怕表被改成更大的每件贡献,也不能溢出分母。
func TestCompoundSuccessRateIsCappedAtFullOdds(t *testing.T) {
recipe := CompoundRecipe{Rarity: 0, MaxMaterialCount: 4, RatePerMaterial: 9000}
if got := recipe.SuccessRateFor(4); got != CompoundRateDenominator {
t.Fatalf("rate = %d, want the %d cap", got, CompoundRateDenominator)
}
}