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,430 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
)
|
||||
|
||||
func newRuntimeRaidSquadSyncFixture(t *testing.T, members []runtimeRaidMemberState) runtimeRaidDisconnectFixture {
|
||||
t.Helper()
|
||||
ids := make([]uint16, 0, len(members))
|
||||
for _, member := range members {
|
||||
ids = append(ids, member.CharID)
|
||||
}
|
||||
fixture := newRuntimeRaidDisconnectFixture(t, ids...)
|
||||
fixture.service.raidMu.Lock()
|
||||
fixture.service.raids[fixture.raidKey].Members = append([]runtimeRaidMemberState(nil), members...)
|
||||
fixture.service.raidMu.Unlock()
|
||||
return fixture
|
||||
}
|
||||
|
||||
func (f runtimeRaidDisconnectFixture) group(t *testing.T, id uint16) byte {
|
||||
t.Helper()
|
||||
state := f.state(t)
|
||||
index := runtimeRaidMemberIndex(state, id)
|
||||
if index < 0 {
|
||||
t.Fatalf("member %d missing", id)
|
||||
}
|
||||
return state.Members[index].GroupIndex
|
||||
}
|
||||
|
||||
func TestRuntimeRaidSquadFollowsOrdinaryPartyFormation(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 2, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1003, GroupIndex: 3, SlotOrder: 0, UserState: 1},
|
||||
})
|
||||
fixture.sessions[1003].conn.(*bufferConn).write.Reset()
|
||||
|
||||
// 1001 and 1002 form an ordinary party; a stranger (4242) in that party is
|
||||
// ignored. They end up in one squad, the one 1001 already had room in.
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 1002, 4242}) {
|
||||
t.Fatal("ordinary party formation did not change the raid roster")
|
||||
}
|
||||
if fixture.group(t, 1001) != fixture.group(t, 1002) || fixture.group(t, 1001) != 1 {
|
||||
t.Fatalf("squads after formation: 1001=%d 1002=%d", fixture.group(t, 1001), fixture.group(t, 1002))
|
||||
}
|
||||
if fixture.group(t, 1003) != 3 {
|
||||
t.Fatal("a member outside the ordinary party was moved")
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, fixture.state(t))
|
||||
if _, ok := runtimeRaidPeerFind(runtimeRaidPeerTake(t, fixture.sessions[1003]), raid.RaidMemberRefreshMsgID); !ok {
|
||||
t.Fatal("other members received no roster refresh after the squad sync")
|
||||
}
|
||||
// Publishing the same party again is a no-op.
|
||||
if fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 1002}) {
|
||||
t.Fatal("an unchanged ordinary party re-labeled the squad")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRaidSquadSyncRespectsCapacityAndFrozenRoster(t *testing.T) {
|
||||
// Squad 1 already holds three others, so 1001 cannot host the party there;
|
||||
// the partner's squad 3 has room and inherits the label.
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 3, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 2001, GroupIndex: 1, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 2002, GroupIndex: 1, SlotOrder: 2, UserState: 1},
|
||||
{CharID: 2003, GroupIndex: 1, SlotOrder: 3, UserState: 1},
|
||||
})
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 1002}) {
|
||||
t.Fatal("party formation with a full home squad did not relabel")
|
||||
}
|
||||
if fixture.group(t, 1001) != 3 || fixture.group(t, 1002) != 3 {
|
||||
t.Fatalf("squads after capacity-limited formation: 1001=%d 1002=%d", fixture.group(t, 1001), fixture.group(t, 1002))
|
||||
}
|
||||
// Two members whose squads are both full end up in the lowest free squad.
|
||||
fixture.service.raidMu.Lock()
|
||||
fixture.service.raids[fixture.raidKey].Members = []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 2001, GroupIndex: 1, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 2002, GroupIndex: 1, SlotOrder: 2, UserState: 1},
|
||||
{CharID: 2003, GroupIndex: 1, SlotOrder: 3, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 2, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1003, GroupIndex: 2, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 2004, GroupIndex: 2, SlotOrder: 2, UserState: 1},
|
||||
{CharID: 2005, GroupIndex: 2, SlotOrder: 3, UserState: 1},
|
||||
}
|
||||
fixture.service.raidMu.Unlock()
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 1002, 1003}) {
|
||||
t.Fatal("party across two full squads did not relabel")
|
||||
}
|
||||
if fixture.group(t, 1001) != 3 || fixture.group(t, 1002) != 3 || fixture.group(t, 1003) != 3 {
|
||||
t.Fatalf("squads after both home squads were full: %d/%d/%d", fixture.group(t, 1001), fixture.group(t, 1002), fixture.group(t, 1003))
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, fixture.state(t))
|
||||
|
||||
// During an active attack the raid window's own squad controls are frozen, but
|
||||
// an ordinary party still drags the roster: it is the only way left to pull a
|
||||
// member into a squad once the raid has started.
|
||||
fixture.service.raidMu.Lock()
|
||||
state := fixture.service.raids[fixture.raidKey]
|
||||
state.Started = true
|
||||
state.FrozenMembers = append([]runtimeRaidMemberState(nil), state.Members...)
|
||||
state.Lifecycle.Status = runtimeRaidLifecyclePhaseActive
|
||||
state.Lifecycle.Phase = runtimeRaidDungeonPhaseOne
|
||||
fixture.service.raidMu.Unlock()
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1002, 2001}) {
|
||||
t.Fatal("active attack refused a squad relabel from an ordinary party")
|
||||
}
|
||||
active := fixture.state(t)
|
||||
live := active.Members[runtimeRaidMemberIndex(active, 2001)]
|
||||
frozen, _ := runtimeRaidFrozenMember(active, 2001)
|
||||
if live.GroupIndex != fixture.group(t, 1002) {
|
||||
t.Fatalf("invited member group=%d partner_group=%d", live.GroupIndex, fixture.group(t, 1002))
|
||||
}
|
||||
if live.GroupIndex != frozen.GroupIndex || live.SlotOrder != frozen.SlotOrder {
|
||||
t.Fatalf("the frozen roster did not follow the live one: live=%+v frozen=%+v", live, frozen)
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, active)
|
||||
|
||||
// A settlement in progress has already frozen the reward roster, so the party
|
||||
// no longer reshuffles squads.
|
||||
fixture.service.raidMu.Lock()
|
||||
state = fixture.service.raids[fixture.raidKey]
|
||||
state.Lifecycle.Status = runtimeRaidLifecycleAwaitingRewards
|
||||
fixture.service.raidMu.Unlock()
|
||||
if fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 2002}) {
|
||||
t.Fatal("a settlement in progress accepted a squad relabel")
|
||||
}
|
||||
|
||||
// In phase-one standby the projection resumes.
|
||||
fixture.service.raidMu.Lock()
|
||||
state.Lifecycle.Status = runtimeRaidLifecycleStandby
|
||||
fixture.service.raidMu.Unlock()
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 2002}) {
|
||||
t.Fatal("standby refused a squad relabel from an ordinary party")
|
||||
}
|
||||
after := fixture.state(t)
|
||||
standbyLive := after.Members[runtimeRaidMemberIndex(after, 2002)]
|
||||
standbyFrozen, _ := runtimeRaidFrozenMember(after, 2002)
|
||||
if standbyLive.GroupIndex != standbyFrozen.GroupIndex || standbyLive.SlotOrder != standbyFrozen.SlotOrder || standbyLive.GroupIndex != fixture.group(t, 1001) {
|
||||
t.Fatalf("live=%+v frozen=%+v partner_group=%d", standbyLive, standbyFrozen, fixture.group(t, 1001))
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, after)
|
||||
}
|
||||
|
||||
func TestRuntimeRaidSquadSyncIgnoresCrossRaidParties(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 2, SlotOrder: 0, UserState: 1},
|
||||
})
|
||||
otherKey := fixture.raidKey + 1
|
||||
fixture.service.raidMu.Lock()
|
||||
fixture.service.raids[otherKey] = &runtimeRaidState{
|
||||
RaidKey: otherKey,
|
||||
LeaderID: 3001,
|
||||
TailFlag: 5,
|
||||
Members: []runtimeRaidMemberState{{CharID: 3001, GroupIndex: 1, SlotOrder: 0, UserState: 1}},
|
||||
}
|
||||
fixture.service.raidMu.Unlock()
|
||||
if fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1001, 3001}) {
|
||||
t.Fatal("members of two different raids were relabeled through one ordinary party")
|
||||
}
|
||||
if fixture.group(t, 1001) != 1 {
|
||||
t.Fatal("cross-raid party moved a member")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadKeepsItsNumberWhenAKickedMemberIsStillLabelled is the live
|
||||
// 2026-09-20 report "从1队踢了个人 然后由2队队长邀请进队 进了2队 然后2队踢出这个人
|
||||
// 2队变3队了 这种情况应保持2队不变", raid 1507329:
|
||||
//
|
||||
// 13:12:02 game-raid-squad-synced-from-ordinary-party squad=2 members=[21 22 20]
|
||||
// 13:12:09 game-raid-squad-synced-from-ordinary-party squad=3 members=[21 22]
|
||||
// 13:12:09 game-raid-squad-unassigned-on-party-leave char_id=20 previous_group=2
|
||||
//
|
||||
// The kick clears the leaver's label, but the party snapshot is published first, so squad
|
||||
// 2 still named him when the sync ran and the party was relocated. The party's own squad
|
||||
// must win as long as the party still fits in it.
|
||||
func TestRuntimeRaidSquadKeepsItsNumberWhenAKickedMemberIsStillLabelled(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 21, GroupIndex: 2, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 22, GroupIndex: 2, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 20, GroupIndex: 2, SlotOrder: 2, UserState: 1},
|
||||
{CharID: 1, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
})
|
||||
// The sync must leave both members where they are: nothing to change, and above all
|
||||
// no relocation.
|
||||
fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{21, 22})
|
||||
if got := fixture.group(t, 21); got != 2 {
|
||||
t.Fatalf("the party moved to squad %d, want to keep squad 2", got)
|
||||
}
|
||||
if got := fixture.group(t, 22); got != 2 {
|
||||
t.Fatalf("the partner moved to squad %d, want to keep squad 2", got)
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, fixture.state(t))
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadKeepsTheLeadersNumberOverAnotherMembers pins the "队长则保持队伍号
|
||||
// 不变" half of the rule, and that the leaderless wrapper still answers with the lowest
|
||||
// squad the party holds.
|
||||
func TestRuntimeRaidSquadKeepsTheLeadersNumberOverAnotherMembers(t *testing.T) {
|
||||
state := runtimeRaidState{Members: []runtimeRaidMemberState{
|
||||
{CharID: 21, GroupIndex: 3, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 22, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
}}
|
||||
if got, ok := runtimeRaidSquadForOrdinaryPartyLedBy(state, []uint16{21, 22}, 21); !ok || got != 3 {
|
||||
t.Fatalf("the leader's squad was not kept: squad=%d ok=%t, want 3", got, ok)
|
||||
}
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(state, []uint16{21, 22}); !ok || got != 1 {
|
||||
t.Fatalf("without a leader the lowest held squad must win: squad=%d ok=%t, want 1", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadForOrdinaryPartyFollowsTheOperatorsNumbering states the
|
||||
// operator's numbering rules directly (2026-09-20):
|
||||
//
|
||||
// "单人和单人的组队 应该找一个最小的数字组成新的小队 比如1 2队都被占用 就自动组成3队"
|
||||
// "1 2 3小队都被占用 2队解散了 原2队成员全部变单人 此时 3队不要动 还是3队 …
|
||||
// 再重新组队还是从2队开始"
|
||||
// "队长则保持队伍号不变"
|
||||
func TestRuntimeRaidSquadForOrdinaryPartyFollowsTheOperatorsNumbering(t *testing.T) {
|
||||
stateWith := func(members ...runtimeRaidMemberState) runtimeRaidState {
|
||||
return runtimeRaidState{Members: members}
|
||||
}
|
||||
squad := func(id uint16, group byte) runtimeRaidMemberState {
|
||||
return runtimeRaidMemberState{CharID: id, GroupIndex: group, UserState: 1}
|
||||
}
|
||||
|
||||
// Two unassigned solos: squad 1 is free, so they become squad 1. An unassigned
|
||||
// member occupies nothing -- folding GroupIndex 0 onto squad 1 is exactly what
|
||||
// used to drag every fresh pairing into squad 1.
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(
|
||||
stateWith(squad(1, 0), squad(2, 0), squad(3, 0)),
|
||||
[]uint16{1, 2},
|
||||
); !ok || got != 1 {
|
||||
t.Fatalf("two solos landed in squad %d (ok=%t), want 1", got, ok)
|
||||
}
|
||||
|
||||
// Squads 1 and 2 are held by two other parties: the new party becomes squad 3,
|
||||
// never a free slot inside somebody else's squad.
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(
|
||||
stateWith(squad(11, 1), squad(12, 1), squad(21, 2), squad(31, 0), squad(32, 0)),
|
||||
[]uint16{31, 32},
|
||||
); !ok || got != 3 {
|
||||
t.Fatalf("a new party next to squads 1 and 2 landed in squad %d (ok=%t), want 3", got, ok)
|
||||
}
|
||||
|
||||
// 1, 2 and 3 occupied and squad 2 disbands: squad 3 is left alone and the next
|
||||
// party starts again from the freed number 2.
|
||||
disbanded := stateWith(squad(11, 1), squad(21, 0), squad(22, 0), squad(31, 3), squad(41, 0), squad(42, 0))
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(disbanded, []uint16{41, 42}); !ok || got != 2 {
|
||||
t.Fatalf("after squad 2 disbanded the next party landed in squad %d (ok=%t), want 2", got, ok)
|
||||
}
|
||||
// With every squad held by another party there is no home left: the roster is
|
||||
// left alone rather than folding the party into somebody else's squad.
|
||||
full := runtimeRaidState{}
|
||||
for group := byte(1); group <= runtimeRaidMaxGroups; group++ {
|
||||
full.Members = append(full.Members, runtimeRaidMemberState{
|
||||
CharID: uint16(100) + uint16(group), GroupIndex: group, UserState: 1,
|
||||
})
|
||||
}
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(full, []uint16{901, 902}); ok {
|
||||
t.Fatalf("a party was folded into the occupied squad %d although every squad was taken", got)
|
||||
}
|
||||
|
||||
// A party that already holds a squad keeps that number even though a lower one
|
||||
// is free ("队长则保持队伍号不变"), as long as no outsider stands in it.
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(
|
||||
stateWith(squad(61, 2), squad(62, 2), squad(71, 4)),
|
||||
[]uint16{61, 62},
|
||||
); !ok || got != 2 {
|
||||
t.Fatalf("a party holding squad 2 moved to squad %d (ok=%t), want to stay in 2", got, ok)
|
||||
}
|
||||
|
||||
// ...but a squad shared with an outsider is not the party's home: the party
|
||||
// moves out to the lowest squad nobody else occupies.
|
||||
if got, ok := runtimeRaidSquadForOrdinaryParty(
|
||||
stateWith(squad(61, 1), squad(62, 1), squad(63, 0), squad(64, 0)),
|
||||
[]uint16{63, 64},
|
||||
); !ok || got != 2 {
|
||||
t.Fatalf("a party joined a squad that already held an outsider (squad %d), want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadSyncNeverFoldsANewPartyIntoAnOccupiedSquad is the live
|
||||
// 2026-09-20 regression, raid 1507338:
|
||||
//
|
||||
// 10:56:51 game-raid-squad-synced-from-ordinary-party squad=1 members=[13 9]
|
||||
// 10:56:55 game-raid-squad-synced-from-ordinary-party squad=1 members=[10 12] ← wrong
|
||||
//
|
||||
// 13 and 9 were a different ordinary party and were standing in squad 1 when
|
||||
// [10 12] formed, so the roster ended up claiming one squad for two parties --
|
||||
// the operator's "多队伍成员错误显示为同属一支队伍".
|
||||
func TestRuntimeRaidSquadSyncNeverFoldsANewPartyIntoAnOccupiedSquad(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 9, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 13, GroupIndex: 1, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 10, GroupIndex: 0, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 12, GroupIndex: 0, SlotOrder: 0, UserState: 1},
|
||||
})
|
||||
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{10, 12}) {
|
||||
t.Fatal("the new ordinary party did not change the raid roster")
|
||||
}
|
||||
if got := fixture.group(t, 10); got != 2 {
|
||||
t.Fatalf("the new party landed in squad %d, want squad 2 beside the party already in squad 1", got)
|
||||
}
|
||||
if got := fixture.group(t, 12); got != 2 {
|
||||
t.Fatalf("the partner landed in squad %d, want squad 2", got)
|
||||
}
|
||||
if fixture.group(t, 9) != 1 || fixture.group(t, 13) != 1 {
|
||||
t.Fatalf("the squad already holding a party was disturbed: 9=%d 13=%d",
|
||||
fixture.group(t, 9), fixture.group(t, 13))
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, fixture.state(t))
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadNumberSurvivesMemberDepartureAndIsReused pins the other half
|
||||
// of the operator's rule: "被踢出的人变单人 而队长则保持队伍号不变 … 队长才会变单人
|
||||
// 小队号这就空出来了", and the freed number is the first one a later party takes.
|
||||
func TestRuntimeRaidSquadNumberSurvivesMemberDepartureAndIsReused(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 2, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 2, SlotOrder: 1, UserState: 1},
|
||||
{CharID: 2001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 3001, GroupIndex: 3, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 4001, GroupIndex: 0, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 4002, GroupIndex: 0, SlotOrder: 0, UserState: 1},
|
||||
})
|
||||
|
||||
// The member walks out of the squad's party: only he becomes solo, the leader
|
||||
// keeps squad 2.
|
||||
if !fixture.service.unassignRuntimeRaidSquadOnPartyLeave(1002) {
|
||||
t.Fatal("the departing party member kept its squad label")
|
||||
}
|
||||
if got := fixture.group(t, 1001); got != 2 {
|
||||
t.Fatalf("the squad leader moved to squad %d, want to keep 2", got)
|
||||
}
|
||||
if got := fixture.group(t, 1002); got != 0 {
|
||||
t.Fatalf("the member who left the party stayed in squad %d, want solo (0)", got)
|
||||
}
|
||||
|
||||
// A fresh party takes the lowest number nobody else occupies: 1, 2 and 3 are
|
||||
// all taken, so it becomes squad 4.
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{4001, 4002}) {
|
||||
t.Fatal("a fresh party of two solos did not change the raid roster")
|
||||
}
|
||||
if got := fixture.group(t, 4001); got != 4 {
|
||||
t.Fatalf("a fresh party took squad %d while 1/2/3 were occupied, want 4", got)
|
||||
}
|
||||
|
||||
// The leader leaves too: squad 2 is now empty and the next party reuses it.
|
||||
if !fixture.service.unassignRuntimeRaidSquadOnPartyLeave(1001) {
|
||||
t.Fatal("the squad leader kept its squad label after leaving the party")
|
||||
}
|
||||
if got := fixture.group(t, 1001); got != 0 {
|
||||
t.Fatalf("the leader who left alone stayed in squad %d, want solo (0)", got)
|
||||
}
|
||||
if !fixture.service.syncRuntimeRaidSquadsWithOrdinaryParty([]uint16{1002, 1001}) {
|
||||
t.Fatal("the party re-formed around the freed squad number")
|
||||
}
|
||||
if got := fixture.group(t, 1001); got != 2 {
|
||||
t.Fatalf("the re-formed party took squad %d, want the freed 2", got)
|
||||
}
|
||||
if got := fixture.group(t, 1002); got != 2 {
|
||||
t.Fatalf("the partner joined squad %d, want 2", got)
|
||||
}
|
||||
assertRuntimeRaidGridUnique(t, fixture.state(t))
|
||||
}
|
||||
|
||||
// TestRuntimeRaidSquadClearedOnSessionLeaveInsideAGate pins the deliberate
|
||||
// difference between the two clears. An ordinary party leave is REFUSED while the
|
||||
// member stands inside a raid gate, because the party object there is republished
|
||||
// by the entry/barrier machinery rather than by a player gesture. A connection
|
||||
// going away is a real leave even inside one: its gate run is released a few
|
||||
// statements later in the same teardown (noteRuntimeRaidSessionDisconnected ->
|
||||
// releaseRuntimeRaidRunsOwnedByLocked), so the guard would make every raid
|
||||
// disconnect a no-op. Operator rule 2026-09-27: 「攻坚队被顶号 掉线 本来就要掉编制」.
|
||||
func TestRuntimeRaidSquadClearedOnSessionLeaveInsideAGate(t *testing.T) {
|
||||
fixture := newRuntimeRaidSquadSyncFixture(t, []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0, UserState: 1},
|
||||
{CharID: 1002, GroupIndex: 1, SlotOrder: 1, UserState: 1},
|
||||
})
|
||||
service := fixture.service
|
||||
service.raidMu.Lock()
|
||||
state := service.raids[fixture.raidKey]
|
||||
state.FrozenMembers = append([]runtimeRaidMemberState(nil), state.Members...)
|
||||
service.raidMu.Unlock()
|
||||
|
||||
// 1002 is fighting inside one of the raid's own gates.
|
||||
session := fixture.sessions[1002]
|
||||
session.dungeon.mu.Lock()
|
||||
runtime := &runtimeDungeonState{}
|
||||
runtime.Character.CharacterID = "1002"
|
||||
runtime.raidDungeon.RaidKey = fixture.raidKey
|
||||
session.dungeon.runtime = runtime
|
||||
session.dungeon.mu.Unlock()
|
||||
|
||||
// The party-leave variant refuses: nothing about that gesture says he left.
|
||||
if service.unassignRuntimeRaidSquadOnPartyLeave(1002) {
|
||||
t.Fatal("the party-leave clear unassigned a member standing inside a gate")
|
||||
}
|
||||
if got := fixture.group(t, 1002); got != 1 {
|
||||
t.Fatalf("group=%d after the refused clear, want 1", got)
|
||||
}
|
||||
|
||||
// A connection going away is a leave regardless of where he stands.
|
||||
if !service.unassignRuntimeRaidSquadOnSessionLeave(1002, "test_session_leave") {
|
||||
t.Fatal("the session-leave clear did not unassign a member inside a gate")
|
||||
}
|
||||
if got := fixture.group(t, 1002); got != 0 {
|
||||
t.Fatalf("group=%d after the session leave, want standby 0", got)
|
||||
}
|
||||
if got := fixture.group(t, 1001); got != 1 {
|
||||
t.Fatalf("the squadmate lost its squad: %d", got)
|
||||
}
|
||||
// Live row only: the frozen phase roster keeps crediting him while he still
|
||||
// holds a seat inside the disconnect grace window.
|
||||
after := fixture.state(t)
|
||||
if frozen, found := runtimeRaidFrozenMember(after, 1002); !found || frozen.GroupIndex != 1 {
|
||||
t.Fatalf("frozen roster=%+v found=%t, want the phase row kept at squad 1", frozen, found)
|
||||
}
|
||||
if !service.unassignRuntimeRaidSquadOnSessionLeave(1002, "test_session_leave_again") {
|
||||
// Already standby: a second teardown must simply report no change.
|
||||
return
|
||||
}
|
||||
t.Fatal("an already standby member was unassigned twice by the session leave")
|
||||
}
|
||||
Reference in new issue
Block a user