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,217 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
func markRuntimeAsForgottenLand(runtime *runtimeDungeonState) {
|
||||
runtime.Dungeon.Metadata.DungeonType = "lost land"
|
||||
runtime.Dungeon.Metadata.LostLandParameters = worldmap.LostLandParameters{
|
||||
DayMilliseconds: 120000, NightMilliseconds: 120000, Set: true,
|
||||
}
|
||||
runtime.Dungeon.Metadata.LimitTime = worldmap.DungeonLimitTime{
|
||||
PrimarySeconds: 600, SecondarySeconds: 1200, Set: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCurrentForgottenLandTimeInfoBodyMatchesCurrentEXEReader(t *testing.T) {
|
||||
body := buildCurrentForgottenLandTimeInfoBody(600, 1200)
|
||||
if len(body) != 8 || binary.LittleEndian.Uint32(body[:4]) != 600 || binary.LittleEndian.Uint32(body[4:]) != 1200 {
|
||||
t.Fatalf("time info body=%x", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentForgottenLandTimeProjectionUsesTypedPVFLimits(t *testing.T) {
|
||||
service, session, runtime := newBackToVillageRuntime(t)
|
||||
markRuntimeAsForgottenLand(runtime)
|
||||
if err := service.sendCurrentForgottenLandTimeProjection(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
packet, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != currentForgottenLandTimeInfoMsgID ||
|
||||
!bytes.Equal(packet.Body, buildCurrentForgottenLandTimeInfoBody(600, 1200)) || len(rest) != 0 {
|
||||
t.Fatalf("projection header=%+v body=%x trailing=%x", packet.Header, packet.Body, rest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForgottenLandEntryProjectsTimeBeforeStartMapAndArmsOneTimer(t *testing.T) {
|
||||
source := bridgeDungeonPVF(false)
|
||||
source["dungeon/test.dgn"] = strings.Replace(
|
||||
source["dungeon/test.dgn"],
|
||||
"[basis level]\n20\n",
|
||||
"[basis level]\n20\n[dungeon type]\n`lost land`\n[lost land parameters]\n120000 120000\n[clear condition]\n[limit time]\n600 1200\n[/clear condition]\n",
|
||||
1,
|
||||
)
|
||||
table, resolver, monsters := loadBridgeDungeonStaticData(t, source)
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
||||
CharacterID: "99", AccountID: "account-1", Level: 20,
|
||||
Stats: map[string]int64{
|
||||
"fatigue": 100, "town_id": 38, "area_id": 1,
|
||||
"pos_x": 450, "pos_y": 234, "direction": 5, "area_state": 3,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
||||
CharacterID: "99", Slots: map[string]dnfrepo.ItemStack{},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service := &Service{
|
||||
options: options{accountID: "account-1", gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
worldMapTable: table, worldMapResolver: resolver, dungeonMonsterTable: monsters,
|
||||
dungeonChoice: func(int) (int, error) { return 0, nil },
|
||||
dungeonSeed: func() (uint32, error) { return 0x12345678, nil },
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
gameplayTimers: queue,
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: connection, connID: "forgotten-land-entry-test", selectedCharacterID: 99,
|
||||
selectPreviewObjectStateSent: true, currentSceneObjectListSent: true,
|
||||
}
|
||||
bindDungeonSelectorOriginForTestAt(t, service, session, 38, 1, 450, 234)
|
||||
body := make([]byte, 21)
|
||||
binary.LittleEndian.PutUint32(body[:4], 700)
|
||||
body[4] = 1
|
||||
if err := service.handleDungeonSelectUpper(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
packets := splitAllCurrentGameServerUpperPackets(t, connection.write.Bytes())
|
||||
infoIndex, timeIndex, startIndex := -1, -1, -1
|
||||
for index, packet := range packets {
|
||||
switch packet.Header.MsgID {
|
||||
case currentDungeonInfoNotification:
|
||||
infoIndex = index
|
||||
case currentForgottenLandTimeInfoMsgID:
|
||||
timeIndex = index
|
||||
if packet.Header.Classification != 0 || !bytes.Equal(packet.Body, buildCurrentForgottenLandTimeInfoBody(600, 1200)) {
|
||||
t.Fatalf("time projection header=%+v body=%x", packet.Header, packet.Body)
|
||||
}
|
||||
case currentDungeonStartNotification:
|
||||
startIndex = index
|
||||
}
|
||||
}
|
||||
if infoIndex < 0 || timeIndex <= infoIndex || startIndex <= timeIndex {
|
||||
t.Fatalf("entry packet indexes op28=%d op603=%d op29=%d", infoIndex, timeIndex, startIndex)
|
||||
}
|
||||
queue.mu.Lock()
|
||||
forgottenLandTasks := 0
|
||||
for _, task := range queue.tasks {
|
||||
if strings.HasPrefix(task.name, "dnf-forgotten-land:") {
|
||||
forgottenLandTasks++
|
||||
}
|
||||
}
|
||||
forgottenLandTimerActive := queue.active[session.dungeon.runtime.forgottenLandTimerName] != nil
|
||||
queue.mu.Unlock()
|
||||
if forgottenLandTasks != 1 || !forgottenLandTimerActive {
|
||||
t.Fatalf("forgotten land tasks=%d active=%t timer=%q", forgottenLandTasks, forgottenLandTimerActive, session.dungeon.runtime.forgottenLandTimerName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentForgottenLandTimerUsesGameplayQueueAndDoesNotReset(t *testing.T) {
|
||||
service, session, runtime := newBackToVillageRuntime(t)
|
||||
markRuntimeAsForgottenLand(runtime)
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service.gameplayTimers = queue
|
||||
|
||||
if err := service.armCurrentForgottenLandTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstDeadline := runtime.forgottenLandDeadline
|
||||
if err := service.armCurrentForgottenLandTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scheduled, cancelled, active := queue.counts()
|
||||
if scheduled != 1 || cancelled != 0 || active != 1 {
|
||||
t.Fatalf("timer counts scheduled=%d cancelled=%d active=%d", scheduled, cancelled, active)
|
||||
}
|
||||
if runtime.forgottenLandDeadline != firstDeadline || firstDeadline.Sub(queue.Now()) != 600*time.Second {
|
||||
t.Fatalf("deadline=%s first=%s now=%s", runtime.forgottenLandDeadline, firstDeadline, queue.Now())
|
||||
}
|
||||
task := queue.task(t, 0)
|
||||
if task.delay != 600*time.Second {
|
||||
t.Fatalf("timer delay=%s", task.delay)
|
||||
}
|
||||
|
||||
connection := session.conn.(*bufferConn)
|
||||
connection.write.Reset()
|
||||
queue.fire(task, false)
|
||||
if got := townPostTransitionPacketSignatures(t, connection.write.Bytes()); !reflect.DeepEqual(
|
||||
got,
|
||||
[]string{"op42", "op376", "mode0", "mode1", "op205", "op24", "op559", "op33weakness"},
|
||||
) {
|
||||
t.Fatalf("forgotten-land timeout full route=%v", got)
|
||||
}
|
||||
if !runtime.townReturnPending || !runtime.townReturnOp24Sent || runtime.forgottenLandTimerActive ||
|
||||
session.dungeon.runtime != nil || runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunAbandoned {
|
||||
t.Fatalf("timeout state pending=%t op24=%t timer_active=%t", runtime.townReturnPending, runtime.townReturnOp24Sent, runtime.forgottenLandTimerActive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledForgottenLandTimerCannotAffectReplacementRuntime(t *testing.T) {
|
||||
service, session, runtime := newBackToVillageRuntime(t)
|
||||
markRuntimeAsForgottenLand(runtime)
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service.gameplayTimers = queue
|
||||
if err := service.armCurrentForgottenLandTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := queue.task(t, 0)
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
service.cancelCurrentForgottenLandTimerLocked(session, runtime, "test_runtime_replaced")
|
||||
session.dungeon.runtime = nil
|
||||
session.dungeon.mu.Unlock()
|
||||
session.conn.(*bufferConn).write.Reset()
|
||||
queue.fire(task, true)
|
||||
if got := session.conn.(*bufferConn).write.Len(); got != 0 {
|
||||
t.Fatalf("stale timer wrote %d bytes", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterruptedTownReturnCancelsForgottenLandTimerAndRetiresRun(t *testing.T) {
|
||||
service, session, runtime := newBackToVillageRuntime(t)
|
||||
markRuntimeAsForgottenLand(runtime)
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service.gameplayTimers = queue
|
||||
if err := service.armCurrentForgottenLandTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := queue.task(t, 0)
|
||||
|
||||
connection := session.conn.(*bufferConn)
|
||||
connection.write.Reset()
|
||||
if err := service.handleDungeonBackToVillage(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, cancelled, _ := queue.counts()
|
||||
queue.mu.Lock()
|
||||
timerStillActive := queue.active[task.name] != nil
|
||||
queue.mu.Unlock()
|
||||
if !runtime.townReturnPending || runtime.forgottenLandTimerActive || cancelled != 1 || timerStillActive ||
|
||||
session.dungeon.runtime != nil || runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunAbandoned {
|
||||
t.Fatalf("full return pending=%t timer_active=%t cancelled=%d scheduled=%t owner=%p status=%s",
|
||||
runtime.townReturnPending, runtime.forgottenLandTimerActive, cancelled, timerStillActive,
|
||||
session.dungeon.runtime, runtime.Session.Snapshot().Run.Status)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
queue.fire(task, true)
|
||||
if connection.write.Len() != 0 {
|
||||
t.Fatalf("stale forgotten-land timer wrote after retirement=%x", connection.write.Bytes())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user