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,288 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
// lukeGeneratorTestConfig carries the two luke.etc tables the generator logic
|
||||
// reads, verbatim: [DEFAULT OBJECT COUNT] and [1PHASE OBJ DESTROY].
|
||||
func lukeGeneratorTestConfig() *raid.LukeConfig {
|
||||
return &raid.LukeConfig{
|
||||
SanctumObjectCounts: []raid.LukeSanctumObjectCount{
|
||||
{Section: "birth sanctum", Count: 5, DungeonID: 5054},
|
||||
{Section: "extinction sanctum", Count: 5, DungeonID: 5055},
|
||||
{Section: "spread sanctum", Count: 5, DungeonID: 5056},
|
||||
},
|
||||
PhaseOneObjectDestroy: []raid.LukePhaseOneObjectDestroy{
|
||||
{Group: 0, Minimum: 0, Maximum: 5},
|
||||
{Group: 1, Minimum: 6, Maximum: 10},
|
||||
{Group: 2, Minimum: 11, Maximum: 15},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func lukePhaseOneGeneratorState() *runtimeRaidState {
|
||||
return &runtimeRaidState{
|
||||
RaidKey: 0x00300001,
|
||||
RaidMode: runtimeRaidOwnerLuke,
|
||||
Started: true,
|
||||
LukeConfig: lukeGeneratorTestConfig(),
|
||||
Lifecycle: runtimeRaidLifecycleState{
|
||||
Status: runtimeRaidLifecyclePhaseActive,
|
||||
Phase: runtimeRaidDungeonPhaseOne,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLukeGeneratorCountsIntoItsSanctumAndSaturatesAtFive(t *testing.T) {
|
||||
state := lukePhaseOneGeneratorState()
|
||||
for i := uint32(1); i <= 5; i++ {
|
||||
count, counted := countRuntimeRaidLukeGeneratorLocked(state, lukeBirthSanctumMapID)
|
||||
if !counted || count.Symbol != lukeSymbolBirthSanctumObject || count.After != i || count.Cap != 5 {
|
||||
t.Fatalf("generator %d: count=%+v counted=%t", i, count, counted)
|
||||
}
|
||||
}
|
||||
if count, counted := countRuntimeRaidLukeGeneratorLocked(state, lukeBirthSanctumMapID); counted || count.After != 5 {
|
||||
t.Fatalf("6th generator: count=%+v counted=%t want saturated at 5", count, counted)
|
||||
}
|
||||
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolExtinctionSanctumObject); got != 0 {
|
||||
t.Fatalf("birth generators leaked into extinction: %d", got)
|
||||
}
|
||||
// Tier of 5 destroyed is 0; five more elsewhere make it 1.
|
||||
if tier := runtimeRaidLukeEnergyControlTierLocked(state); tier != 0 {
|
||||
t.Fatalf("tier after 5 = %d want 0", tier)
|
||||
}
|
||||
countRuntimeRaidLukeGeneratorLocked(state, lukeExtinctionSanctumMapID)
|
||||
if tier := runtimeRaidLukeEnergyControlTierLocked(state); tier != 1 {
|
||||
t.Fatalf("tier after 6 = %d want 1", tier)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLukeGeneratorIgnoredOutsidePhaseOneAndOutsideSanctums(t *testing.T) {
|
||||
state := lukePhaseOneGeneratorState()
|
||||
if _, counted := countRuntimeRaidLukeGeneratorLocked(state, lukeLightAltarMapID); counted {
|
||||
t.Fatal("a phase-two map counted a generator")
|
||||
}
|
||||
state.Lifecycle.Phase = runtimeRaidDungeonPhaseTwo
|
||||
if _, counted := countRuntimeRaidLukeGeneratorLocked(state, lukeBirthSanctumMapID); counted {
|
||||
t.Fatal("phase two counted a sanctum generator")
|
||||
}
|
||||
state.Lifecycle.Phase = runtimeRaidDungeonPhaseOne
|
||||
state.TailFlag = 1
|
||||
if _, counted := countRuntimeRaidLukeGeneratorLocked(state, lukeBirthSanctumMapID); counted {
|
||||
t.Fatal("a single-mode raid counted a generator")
|
||||
}
|
||||
}
|
||||
|
||||
// Every member's client runs the machine's destroy action, so one machine
|
||||
// arrives once per member in the room. A room is credited with the most reports
|
||||
// any single member made there; rooms and runs are independent.
|
||||
func TestLukeGeneratorReportsDeduplicateAcrossMembersPerRoom(t *testing.T) {
|
||||
state := lukePhaseOneGeneratorState()
|
||||
roomA := runtimeRaidLukeGeneratorReportKey{
|
||||
MapID: lukeExtinctionSanctumMapID, LifecycleToken: 7,
|
||||
Room: runtimeDungeonRoomKey{X: 1, Y: 0, MapID: 74139},
|
||||
}
|
||||
roomB := roomA
|
||||
roomB.Room = runtimeDungeonRoomKey{X: 2, Y: 0, MapID: 74140}
|
||||
nextRun := roomA
|
||||
nextRun.LifecycleToken = 8
|
||||
|
||||
steps := []struct {
|
||||
key runtimeRaidLukeGeneratorReportKey
|
||||
member uint16
|
||||
wantFresh bool
|
||||
}{
|
||||
{roomA, 1, true}, // member 1 breaks the first machine
|
||||
{roomA, 42, false}, // member 42's client reports the same machine
|
||||
{roomA, 42, true}, // member 42 breaks a second machine first
|
||||
{roomA, 1, false}, // member 1's copy of that second machine
|
||||
{roomB, 1, true}, // another room is its own ledger
|
||||
{nextRun, 1, true}, // so is the next run of the sanctum
|
||||
{nextRun, 42, false},
|
||||
}
|
||||
for i, step := range steps {
|
||||
_, fresh := recordRuntimeRaidLukeGeneratorReportLocked(state, step.key, step.member)
|
||||
if fresh != step.wantFresh {
|
||||
t.Fatalf("step %d (%+v member %d): fresh=%t want %t", i, step.key, step.member, fresh, step.wantFresh)
|
||||
}
|
||||
if fresh {
|
||||
countRuntimeRaidLukeGeneratorLocked(state, step.key.MapID)
|
||||
}
|
||||
}
|
||||
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolExtinctionSanctumObject); got != 4 {
|
||||
t.Fatalf("extinction counter = %d want 4 (two in room A, one in room B, one next run)", got)
|
||||
}
|
||||
// The snapshot clone must not share the ledger with the live state.
|
||||
clone := cloneRuntimeRaidState(*state)
|
||||
clone.Lifecycle.LukeGeneratorReports[roomA].ByMember[1] = 99
|
||||
if got := state.Lifecycle.LukeGeneratorReports[roomA].ByMember[1]; got != 2 {
|
||||
t.Fatalf("clone aliased the live ledger: member 1 = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// lukeGeneratorRunInSanctum binds one session to a live run of a Luke phase-one
|
||||
// sanctum. localToken is this member's OWN lifecycle token; bindingToken is the
|
||||
// SQUAD's run token, which is what an adopting squadmate's binding carries (see
|
||||
// the adoption branch of beginRuntimeRaidDungeon).
|
||||
func lukeGeneratorRunInSanctum(
|
||||
session *gameSession,
|
||||
charID uint16,
|
||||
raidKey uint32,
|
||||
mapID int,
|
||||
localToken uint64,
|
||||
bindingToken uint64,
|
||||
) *runtimeDungeonState {
|
||||
run := &runtimeDungeonState{
|
||||
Request: dungeoncmd.SelectDungeonRequest{DungeonID: uint32(mapID)},
|
||||
Character: dnfrepo.CharacterRecord{CharacterID: strconv.Itoa(int(charID))},
|
||||
lifecycleToken: localToken,
|
||||
}
|
||||
run.raidDungeon = runtimeRaidDungeonBinding{
|
||||
RaidKey: raidKey,
|
||||
Phase: runtimeRaidDungeonPhaseOne,
|
||||
MapID: mapID,
|
||||
LifecycleToken: bindingToken,
|
||||
}
|
||||
run.Room = &runtimeDungeonRoom{mapID: int64(mapID) + 68600}
|
||||
session.dungeon = dungeonSessionState{runtime: run}
|
||||
return run
|
||||
}
|
||||
|
||||
func lukeGeneratorReportBody(symbol uint32) []byte {
|
||||
body := make([]byte, 9)
|
||||
binary.LittleEndian.PutUint32(body[0:4], symbol)
|
||||
binary.LittleEndian.PutUint32(body[4:8], 1)
|
||||
body[8] = runtimeRaidSymbolOperationAdd
|
||||
return body
|
||||
}
|
||||
|
||||
// lukeGeneratorLiveRaid turns a fixture's roster into a started, active Luke
|
||||
// phase-one raid.
|
||||
func lukeGeneratorLiveRaid(t *testing.T, service *Service, raidKey uint32) {
|
||||
t.Helper()
|
||||
service.raidMu.Lock()
|
||||
defer service.raidMu.Unlock()
|
||||
state := service.raids[raidKey]
|
||||
if state == nil {
|
||||
t.Fatalf("raid %d is not registered", raidKey)
|
||||
}
|
||||
state.RaidKey = raidKey
|
||||
state.RaidMode = runtimeRaidOwnerLuke
|
||||
state.TailFlag = 0
|
||||
state.Started = true
|
||||
state.StartPending = false
|
||||
state.LukeConfig = lukeGeneratorTestConfig()
|
||||
state.Lifecycle.Status = runtimeRaidLifecyclePhaseActive
|
||||
state.Lifecycle.Phase = runtimeRaidDungeonPhaseOne
|
||||
}
|
||||
|
||||
// A machine's destroy action runs on EVERY member's client, and a squadmate that
|
||||
// walked into the opener's run carries the SQUAD's token in its binding while its
|
||||
// own local token is different. Comparing the two here silently dropped that
|
||||
// member's report -- the exact failure upstream had already removed from the Anton
|
||||
// twin (runtimeRaidCurrentRaidDungeonRun, 2026-09-19 raid 1507338) and left in this
|
||||
// Luke helper, which the port carried across:
|
||||
//
|
||||
// live 2026-09-29 09:21-09:23Z, raid 1835009, 湮灭之圣所: char 22 opened the run
|
||||
// (binding token 7), char 1 followed it in (its own token 8). The machine's
|
||||
// [RAID SET SYMBOL] 118 [+] 1 arrived as op662 {118, 1, add} and was declined,
|
||||
// fell through to the Anton barrier owner and was rejected as
|
||||
// not_inside_energy_or_hatchery_run; 破坏目标 stayed 5/5 while the opener's own
|
||||
// reports had counted to 5 earlier the same day.
|
||||
func TestLukeGeneratorReportCountsForAnAdoptedSquadmate(t *testing.T) {
|
||||
fixture := newStressRaidProjectionFixture(t, 2)
|
||||
lukeGeneratorLiveRaid(t, fixture.service, fixture.raidKey)
|
||||
opener, adopted := fixture.members[0], fixture.members[1]
|
||||
|
||||
// The opener's own run: binding token == its local token.
|
||||
lukeGeneratorRunInSanctum(opener.session, opener.charID, fixture.raidKey, lukeExtinctionSanctumMapID, 7, 7)
|
||||
// The squadmate that walked into that same run.
|
||||
lukeGeneratorRunInSanctum(adopted.session, adopted.charID, fixture.raidKey, lukeExtinctionSanctumMapID, 8, 7)
|
||||
|
||||
// One machine per member's client, reported from the adopted member first.
|
||||
for _, member := range []stressRaidProjectionMember{adopted, opener} {
|
||||
if err := fixture.service.handleOnlineRaidSetSymbol(
|
||||
member.session, 662, lukeGeneratorReportBody(lukeSymbolExtinctionSanctumObject),
|
||||
); err != nil {
|
||||
t.Fatalf("char %d generator report: %v", member.charID, err)
|
||||
}
|
||||
}
|
||||
|
||||
fixture.service.raidMu.Lock()
|
||||
defer fixture.service.raidMu.Unlock()
|
||||
live := fixture.service.raids[fixture.raidKey]
|
||||
if got := runtimeRaidLukeSymbolLocked(live, lukeSymbolExtinctionSanctumObject); got != 1 {
|
||||
t.Fatalf("湮灭 generator counter = %d want 1: the adopted member's report was dropped "+
|
||||
"(both clients reported the same machine, so the room credits it once)", got)
|
||||
}
|
||||
// The ledger is keyed on the SQUAD's token, not on either member's local one,
|
||||
// so both members' reports land in the same room entry.
|
||||
key := runtimeRaidLukeGeneratorReportKey{
|
||||
MapID: lukeExtinctionSanctumMapID,
|
||||
LifecycleToken: 7,
|
||||
Room: runtimeDungeonRoomKey{MapID: int64(lukeExtinctionSanctumMapID) + 68600},
|
||||
}
|
||||
room, ok := live.Lifecycle.LukeGeneratorReports[key]
|
||||
if !ok {
|
||||
t.Fatalf("no generator ledger for the squad's run token; entries=%v", live.Lifecycle.LukeGeneratorReports)
|
||||
}
|
||||
if room.ByMember[adopted.charID] != 1 || room.ByMember[opener.charID] != 1 || room.Credited != 1 {
|
||||
t.Fatalf("ledger = %+v want one report from each member and one credited machine", room)
|
||||
}
|
||||
}
|
||||
|
||||
// The Luke claim must keep declining everything that is not a Luke phase-one
|
||||
// sanctum, so Anton's own op662 reaches applyRuntimeRaidBarrierRequest with the
|
||||
// same request at the same moment it always did. Map and symbol both have to miss:
|
||||
// Anton stands in 210/218/21x and reports symbol 110, Luke sanctums are 5054/5055/
|
||||
// 5056 and report 117/118/119.
|
||||
func TestLukeGeneratorClaimDeclinesEveryAntonReport(t *testing.T) {
|
||||
fixture := newStressRaidProjectionFixture(t, 2)
|
||||
lukeGeneratorLiveRaid(t, fixture.service, fixture.raidKey)
|
||||
member := fixture.members[0]
|
||||
|
||||
antonRun := &runtimeDungeonState{
|
||||
Request: dungeoncmd.SelectDungeonRequest{DungeonID: antonEnergyInterceptionMapID},
|
||||
Character: dnfrepo.CharacterRecord{CharacterID: strconv.Itoa(int(member.charID))},
|
||||
lifecycleToken: 11,
|
||||
}
|
||||
antonRun.raidDungeon = runtimeRaidDungeonBinding{
|
||||
RaidKey: fixture.raidKey,
|
||||
Phase: runtimeRaidDungeonPhaseTwo,
|
||||
MapID: antonEnergyInterceptionMapID,
|
||||
LifecycleToken: 11,
|
||||
}
|
||||
antonRun.Room = &runtimeDungeonRoom{mapID: 999}
|
||||
member.session.dungeon = dungeonSessionState{runtime: antonRun}
|
||||
|
||||
reports := []raid.SetSymbolRequest{
|
||||
{Symbol: runtimeRaidSymbolOperationSubtract, SourceValue: antonSymbolBarrier, TargetValue: 1},
|
||||
// Even a Luke symbol counts for nothing on an Anton map ...
|
||||
{Symbol: runtimeRaidSymbolOperationAdd, SourceValue: lukeSymbolExtinctionSanctumObject, TargetValue: 1},
|
||||
}
|
||||
for index, report := range reports {
|
||||
if fixture.service.applyRuntimeRaidLukeGeneratorReport(member.session, report) {
|
||||
t.Fatalf("report %d (%+v) was claimed by the Luke generator on an Anton run", index, report)
|
||||
}
|
||||
}
|
||||
|
||||
// ... and an Anton symbol counts for nothing inside a Luke sanctum.
|
||||
lukeGeneratorRunInSanctum(member.session, member.charID, fixture.raidKey, lukeExtinctionSanctumMapID, 12, 12)
|
||||
if fixture.service.applyRuntimeRaidLukeGeneratorReport(member.session, raid.SetSymbolRequest{
|
||||
Symbol: runtimeRaidSymbolOperationSubtract, SourceValue: antonSymbolBarrier, TargetValue: 1,
|
||||
}) {
|
||||
t.Fatal("Anton's barrier report was claimed inside a Luke sanctum")
|
||||
}
|
||||
fixture.service.raidMu.Lock()
|
||||
defer fixture.service.raidMu.Unlock()
|
||||
if got := runtimeRaidLukeSymbolLocked(fixture.service.raids[fixture.raidKey], lukeSymbolExtinctionSanctumObject); got != 0 {
|
||||
t.Fatalf("a declined report moved the sanctum counter to %d", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user