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,209 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/actionobject"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
// The three 能源储藏室 scripts, verbatim in shape (see actionobject tests for
|
||||
// the transcription). The runtime is exercised through them because their
|
||||
// trigger-switch choreography is exactly what a per-visit model gets wrong.
|
||||
const (
|
||||
testEnergySaveGateScript = "" +
|
||||
"[TRIGGER]\n[LIMIT]\n1\n[ENABLE]\n[ON]\n" +
|
||||
"[DUNGEON SYMBOL COMPARE]\n`storage gate open`\n[<]\n2\n[DO BEHAVIOR]\n[ME]\n0\n[/TRIGGER]\n" +
|
||||
"[BEHAVIOR]\n[NOTICE]\n`gate closed` 255 255 0 `None` 4000 1000\n[/NOTICE]\n" +
|
||||
"[GATE ENABLE]\n-1 -1 0\n[DOWN]\n[/GATE ENABLE]\n[TRIGGER]\n1\n[ON]\n[TRIGGER]\n0\n[OFF]\n[/BEHAVIOR]\n" +
|
||||
"[TRIGGER]\n[LIMIT]\n1\n[ENABLE]\n[OFF]\n" +
|
||||
"[DUNGEON SYMBOL COMPARE]\n`storage gate open`\n[>=]\n2\n[DO BEHAVIOR]\n[ME]\n1\n[/TRIGGER]\n" +
|
||||
"[BEHAVIOR]\n[NOTICE]\n`gate open` 255 255 0 `None` 4000 1000\n[/NOTICE]\n" +
|
||||
"[GATE ENABLE]\n-1 -1 1\n[DOWN]\n[/GATE ENABLE]\n[/BEHAVIOR]\n"
|
||||
testEnergySaveCylinderScript = "" +
|
||||
"[TRIGGER]\n[LIMIT]\n1\n[WHICH]\n[MONSTER]\n[CHECKUP]\n[IS INDEX]\n%d\n[/IS INDEX]\n" +
|
||||
"[CHECKED NO]\n[<]\n1\n[/CHECKUP]\n[DO BEHAVIOR]\n[ME]\n0\n[/TRIGGER]\n" +
|
||||
"[BEHAVIOR]\n[DUNGEON SYMBOL CALC]\n`storage gate open`\n[+]\n1\n[/BEHAVIOR]\n"
|
||||
)
|
||||
|
||||
func testActionObjectScript(t *testing.T, name, text string) actionobject.Script {
|
||||
t.Helper()
|
||||
document, err := dnfpvf.Parse(name, text)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", name, err)
|
||||
}
|
||||
script, err := actionobject.Parse(document)
|
||||
if err != nil {
|
||||
t.Fatalf("decode %s: %v", name, err)
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
||||
func testActionObjectRoom(x, y int64, mapID int64, monsterID int64) *runtimeDungeonRoom {
|
||||
room := &runtimeDungeonRoom{
|
||||
coordinate: worldmap.RoomCoordinate{X: x, Y: y},
|
||||
mapID: mapID,
|
||||
byObjectKey: make(map[uint32]int),
|
||||
}
|
||||
if monsterID != 0 {
|
||||
room.monsters = append(room.monsters, runtimeDungeonMonster{
|
||||
ObjectKey: 1,
|
||||
Spawn: worldmap.MonsterSpawn{MonsterID: monsterID},
|
||||
State: runtimeDungeonMonsterAnnounced,
|
||||
})
|
||||
room.byObjectKey[1] = 0
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
func testActionObjectInstance(objectID int64, key runtimeDungeonRoomKey, script actionobject.Script) *runtimeDungeonActionObjectInstance {
|
||||
instance := &runtimeDungeonActionObjectInstance{
|
||||
ObjectID: objectID,
|
||||
Room: key,
|
||||
Script: script,
|
||||
Triggers: make([]runtimeDungeonActionObjectTrigger, len(script.Triggers)),
|
||||
}
|
||||
for index, trigger := range script.Triggers {
|
||||
instance.Triggers[index].Enabled = trigger.Enabled
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
// TestActionObjectsOpenEnergySaveBossDoorAfterBothCylinders walks the room
|
||||
// order a squad actually takes: start room (door closes), left room kill,
|
||||
// back through start, right room kill, back to start (door opens).
|
||||
func TestActionObjectsOpenEnergySaveBossDoorAfterBothCylinders(t *testing.T) {
|
||||
s := &Service{}
|
||||
start := runtimeDungeonRoomKey{X: 1, Y: 0, MapID: 74249}
|
||||
left := runtimeDungeonRoomKey{X: 0, Y: 0, MapID: 74248}
|
||||
right := runtimeDungeonRoomKey{X: 2, Y: 0, MapID: 74250}
|
||||
|
||||
startRoom := testActionObjectRoom(1, 0, 74249, 0)
|
||||
leftRoom := testActionObjectRoom(0, 0, 74248, 57156)
|
||||
rightRoom := testActionObjectRoom(2, 0, 74250, 56068)
|
||||
runtime := &runtimeDungeonState{
|
||||
Room: startRoom,
|
||||
Rooms: map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit{
|
||||
start: {Room: startRoom},
|
||||
left: {Room: leftRoom},
|
||||
right: {Room: rightRoom},
|
||||
},
|
||||
}
|
||||
state := runtime.actionObjectState()
|
||||
state.Instances = []*runtimeDungeonActionObjectInstance{
|
||||
testActionObjectInstance(64308, start, testActionObjectScript(t, "gate", testEnergySaveGateScript)),
|
||||
testActionObjectInstance(64307, left, testActionObjectScript(t, "left", sprintfScript(testEnergySaveCylinderScript, 57156))),
|
||||
testActionObjectInstance(64306, right, testActionObjectScript(t, "right", sprintfScript(testEnergySaveCylinderScript, 56068))),
|
||||
}
|
||||
|
||||
// Entering the start room: symbol 0 < 2, the door closes and the reopen
|
||||
// trigger is armed.
|
||||
effects := s.evaluateActionObjectsLocked(runtime, start)
|
||||
if effects.GateOpen == nil || *effects.GateOpen {
|
||||
t.Fatalf("start room gate after first entry = %v want closed", effects.GateOpen)
|
||||
}
|
||||
if len(effects.Notices) != 1 || effects.Notices[0].Text != "gate closed" {
|
||||
t.Fatalf("first-entry notices = %+v", effects.Notices)
|
||||
}
|
||||
if !state.Instances[0].Triggers[1].Enabled || state.Instances[0].Triggers[0].Enabled {
|
||||
t.Fatalf("gate triggers after close = %+v", state.Instances[0].Triggers)
|
||||
}
|
||||
|
||||
// Left room: kill 57156. The cylinder counts it and the symbol reaches 1.
|
||||
runtime.Room = leftRoom
|
||||
leftRoom.monsters[0].State = runtimeDungeonMonsterDefeated
|
||||
effects = s.evaluateActionObjectsLocked(runtime, left)
|
||||
if state.Symbols["storage gate open"] != 1 {
|
||||
t.Fatalf("symbol after left kill = %d want 1", state.Symbols["storage gate open"])
|
||||
}
|
||||
if effects.GateOpen != nil {
|
||||
t.Fatalf("left room reported a gate verdict %v; it has no gate script", *effects.GateOpen)
|
||||
}
|
||||
|
||||
// Back through the start room: still short, nothing new fires, door stays shut.
|
||||
runtime.Room = startRoom
|
||||
effects = s.evaluateActionObjectsLocked(runtime, start)
|
||||
if effects.Fired != 0 || effects.GateOpen == nil || *effects.GateOpen {
|
||||
t.Fatalf("start room mid-way: fired=%d gate=%v", effects.Fired, effects.GateOpen)
|
||||
}
|
||||
|
||||
// Right room: kill 56068 -> symbol 2. The gate object is not in this room,
|
||||
// but it is evaluated anyway and flips its own room's gate open now.
|
||||
runtime.Room = rightRoom
|
||||
rightRoom.monsters[0].State = runtimeDungeonMonsterDefeated
|
||||
effects = s.evaluateActionObjectsLocked(runtime, right)
|
||||
if state.Symbols["storage gate open"] != 2 {
|
||||
t.Fatalf("symbol after right kill = %d want 2", state.Symbols["storage gate open"])
|
||||
}
|
||||
if len(effects.Notices) != 0 {
|
||||
t.Fatalf("the opening notice leaked into the right room: %+v", effects.Notices)
|
||||
}
|
||||
if !state.gateOpen[start] {
|
||||
t.Fatal("start room gate did not open once the symbol reached 2")
|
||||
}
|
||||
|
||||
// Walking back into the start room publishes the open verdict.
|
||||
runtime.Room = startRoom
|
||||
effects = s.evaluateActionObjectsLocked(runtime, start)
|
||||
if effects.GateOpen == nil || !*effects.GateOpen {
|
||||
t.Fatalf("start room final gate = %v want open", effects.GateOpen)
|
||||
}
|
||||
// [LIMIT] 1 on both triggers: nothing fires again.
|
||||
if again := s.evaluateActionObjectsLocked(runtime, start); again.Fired != 0 {
|
||||
t.Fatalf("triggers fired past their limit: %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
// TestActionObjectsReopenSurvivesLeavingBeforeSymbolIsMet is the ordering a
|
||||
// per-visit object model breaks: the squad leaves the start room with the
|
||||
// reopen trigger armed, the symbol is completed elsewhere, and the door must
|
||||
// still open on return even though the closing trigger no longer matches.
|
||||
func TestActionObjectsReopenSurvivesLeavingBeforeSymbolIsMet(t *testing.T) {
|
||||
s := &Service{}
|
||||
start := runtimeDungeonRoomKey{X: 1, Y: 0, MapID: 74249}
|
||||
startRoom := testActionObjectRoom(1, 0, 74249, 0)
|
||||
runtime := &runtimeDungeonState{
|
||||
Room: startRoom,
|
||||
Rooms: map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit{start: {Room: startRoom}},
|
||||
}
|
||||
state := runtime.actionObjectState()
|
||||
state.Instances = []*runtimeDungeonActionObjectInstance{
|
||||
testActionObjectInstance(64308, start, testActionObjectScript(t, "gate", testEnergySaveGateScript)),
|
||||
}
|
||||
s.evaluateActionObjectsLocked(runtime, start)
|
||||
// Both cylinders count in while the squad is elsewhere.
|
||||
state.Symbols["storage gate open"] = 2
|
||||
effects := s.evaluateActionObjectsLocked(runtime, start)
|
||||
if effects.GateOpen == nil || !*effects.GateOpen {
|
||||
t.Fatalf("gate after symbol met = %v want open", effects.GateOpen)
|
||||
}
|
||||
if len(effects.Notices) != 1 || effects.Notices[0].Text != "gate open" {
|
||||
t.Fatalf("reopen notices = %+v", effects.Notices)
|
||||
}
|
||||
}
|
||||
|
||||
func sprintfScript(format string, monsterID int64) string {
|
||||
out := make([]byte, 0, len(format)+8)
|
||||
for i := 0; i < len(format); i++ {
|
||||
if format[i] == '%' && i+1 < len(format) && format[i+1] == 'd' {
|
||||
out = append(out, []byte(itoaTest(monsterID))...)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
out = append(out, format[i])
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func itoaTest(value int64) string {
|
||||
if value == 0 {
|
||||
return "0"
|
||||
}
|
||||
var digits []byte
|
||||
for value > 0 {
|
||||
digits = append([]byte{byte('0' + value%10)}, digits...)
|
||||
value /= 10
|
||||
}
|
||||
return string(digits)
|
||||
}
|
||||
Reference in new issue
Block a user