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,134 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
dnfexpertjob "longheng.io/server/internal/modules/dnf/expertjob"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
// TestCurrentExpertJobProjectionRepublishesOnEveryExperienceGain pins the
|
||||
// decision that made 附魔师 crafting look like it did nothing: the profession's
|
||||
// experience only reaches the client through class-0 op205, so an operation
|
||||
// that moved the bar without crossing a level threshold still has to
|
||||
// republish. Gating on LevelChanged alone left the gain invisible until the
|
||||
// next character select replayed the login push.
|
||||
func TestCurrentExpertJobProjectionRepublishesOnEveryExperienceGain(t *testing.T) {
|
||||
for name, probe := range map[string]struct {
|
||||
gain int64
|
||||
levelChanged bool
|
||||
want bool
|
||||
}{
|
||||
"experience_without_level_change": {gain: 12, levelChanged: false, want: true},
|
||||
"experience_with_level_change": {gain: 12, levelChanged: true, want: true},
|
||||
"level_change_without_experience": {gain: 0, levelChanged: true, want: true},
|
||||
"nothing_changed": {gain: 0, levelChanged: false, want: false},
|
||||
} {
|
||||
if got := currentExpertJobProjectionNeedsRepublish(probe.gain, probe.levelChanged); got != probe.want {
|
||||
t.Fatalf("%s: republish=%t, want %t", name, got, probe.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealScriptPVFExpertJobInfoCarriesExperienceWithoutLevelChange proves the
|
||||
// republish is worth sending: two pushes for the same enchanter at the same
|
||||
// level must differ on the wire, because the experience rides in the EJXP
|
||||
// projection trailer the compatibility DLL writes into the current actor.
|
||||
func TestRealScriptPVFExpertJobInfoCarriesExperienceWithoutLevelChange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
pvfPath := filepath.Clean(filepath.Join("..", "..", "..", "..", "runtime", "data", "dnf", "Script.pvf"))
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Skipf("runtime Script.pvf unavailable: %v", err)
|
||||
}
|
||||
expert, err := dnfexpertjob.Load(ctx, archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, ok := expert.Config(dnfexpertjob.EnchanterType)
|
||||
if !ok {
|
||||
t.Fatal("active PVF has no enchanter config")
|
||||
}
|
||||
// Two experience values inside the same level band, so only the trailer
|
||||
// moves and every level-derived field stays identical.
|
||||
const before, after = int64(1), int64(2)
|
||||
if config.Level(before) != config.Level(after) {
|
||||
t.Fatalf("probe experiences straddle a level threshold: %d vs %d", config.Level(before), config.Level(after))
|
||||
}
|
||||
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
character := dnfrepo.CharacterRecord{
|
||||
CharacterID: "7", AccountID: "account-1", Name: "Enchanter",
|
||||
Stats: map[string]int64{"expert_job_type": int64(dnfexpertjob.EnchanterType), "expert_job_exp": before},
|
||||
}
|
||||
if err := repositories.Character.Save(ctx, character); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{
|
||||
accountID: "account-1", gameUpperHeader: gameUpperHeaderChannel13,
|
||||
gameUpperBodyCodec: gameUpperBodyCodecPlain,
|
||||
},
|
||||
expertJobCatalog: expert,
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
}
|
||||
session := &gameSession{conn: connection, selectedCharacterID: 7, accountID: "account-1"}
|
||||
|
||||
if err := service.sendCurrentExpertJobInfoForSelectedCharacter(session, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if len(rest) != 0 || first.Header.MsgID != currentExpertJobInfoNotification {
|
||||
t.Fatalf("first projection header=%+v trailing=%d", first.Header, len(rest))
|
||||
}
|
||||
if got := currentExpertJobTrailerExperience(t, first.Body); got != uint32(before) {
|
||||
t.Fatalf("first projection experience=%d, want %d", got, before)
|
||||
}
|
||||
|
||||
// A craft that does not cross a threshold: the durable row moves, and the
|
||||
// forced republish has to carry it even though the session already saw
|
||||
// this character.
|
||||
character.Stats["expert_job_exp"] = after
|
||||
if err := repositories.Character.Save(ctx, character); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection.write.Reset()
|
||||
if err := service.sendCurrentExpertJobInfoForSelectedCharacter(session, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if len(rest) != 0 || second.Header.MsgID != currentExpertJobInfoNotification {
|
||||
t.Fatalf("second projection header=%+v trailing=%d", second.Header, len(rest))
|
||||
}
|
||||
if got := currentExpertJobTrailerExperience(t, second.Body); got != uint32(after) {
|
||||
t.Fatalf("second projection experience=%d, want %d", got, after)
|
||||
}
|
||||
|
||||
// Without force the same character is suppressed, which is exactly why the
|
||||
// craft path has to ask for force=true.
|
||||
connection.write.Reset()
|
||||
if err := service.sendCurrentExpertJobInfoForSelectedCharacter(session, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if connection.write.Len() != 0 {
|
||||
t.Fatalf("unforced projection re-sent %d bytes", connection.write.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func currentExpertJobTrailerExperience(t *testing.T, body []byte) uint32 {
|
||||
t.Helper()
|
||||
if len(body) < 8 {
|
||||
t.Fatalf("expert job projection body too short: %x", body)
|
||||
}
|
||||
trailer := body[len(body)-8:]
|
||||
if magic := binary.LittleEndian.Uint32(trailer[0:4]); magic != currentExpertJobProjectionTrailerMagic {
|
||||
t.Fatalf("expert job projection trailer magic=%08x body=%x", magic, body)
|
||||
}
|
||||
return binary.LittleEndian.Uint32(trailer[4:8])
|
||||
}
|
||||
Reference in new issue
Block a user