Files
90CNServer/server/internal/services/dnfbridge/weakness_raid_exit_exempt_test.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

164 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"slices"
"testing"
"longheng.io/server/internal/modules/dnf/channelcatalog"
"longheng.io/server/internal/modules/dnf/dnfenum"
)
// 「攻坚战无论怎么中途放弃 都不应该虚弱」(操作员 2026-10-02 定案)——
// 把"这一次未通关回城算不算攻坚战退出"单独钉住。
//
// 真机 2026-10-02 00:00:36(raid 12058625,分片 20261002-001):超时空的**失败收尾**
// 给 char 22 挂上了虚弱 9,而对照的走门那次(00:04:24)是豁免的:
//
// game-dungeon-forced-return-weakness-applied char_id=22
// source=fiendwar_failed_waiting_area_abandoned_run_town_return
// exemption=none weakness_level=9 client_condition=19
// game-dungeon-forced-return-weakness-raid-exempt char_id=1
// source=current_exe_bodyless_op42_active_run_giveup_abandoned_run_town_return
// rule=raid_gate_exit_never_weakens
//
// 差别在于**服务器自己把绑定清空了**:returnRuntimeFiendWarDungeons 的 ended 分支
// 先 `runtime.raidDungeon = runtimeRaidDungeonBinding{}`,于是"看绑定"的那条豁免失效。
func TestForcedReturnWeaknessExemptsEveryRaidExit(t *testing.T) {
service := &Service{}
// 超时空频道(类型 42);普通频道用来做反例。
fiendWar := &gameSession{selectedCharacterID: 22,
channel: channelcatalog.Channel{Type: dnfenum.FiendWarRaidChannelType}}
ordinary := &gameSession{selectedCharacterID: 22,
channel: channelcatalog.Channel{Type: 11}}
// 1) 走门那一种:run 上还带着攻坚绑定 → 豁免。
raidRun := &runtimeDungeonState{raidDungeon: runtimeRaidDungeonBinding{RaidKey: 12058625}}
rule, exempt := service.currentForcedReturnWeaknessRaidExempt(fiendWar, raidRun)
if !exempt || rule != "raid_gate_exit_never_weakens" {
t.Fatalf("带着攻坚绑定的退出被判成要虚弱:rule=%q exempt=%t", rule, exempt)
}
// 2) **绑定已被服务器自己清空**的超时空失败收尾 → 也必须豁免。
// 这就是真机 00:00:36 的那一条。
clearedBinding := &runtimeDungeonState{}
rule, exempt = service.currentForcedReturnWeaknessRaidExempt(fiendWar, clearedBinding)
if !exempt || rule != "raid_channel_exit_never_weakens" {
t.Fatalf("超时空失败收尾(绑定已清空)被判成要虚弱:rule=%q exempt=%t\n"+
"(真机 2026-10-02 00:00:36 就是这条弱点:weakness_level=9 exemption=none)", rule, exempt)
}
// 3) 普通频道、没有攻坚绑定 → 照旧虚弱(这条放松不许漏到普通副本)。
if rule, exempt := service.currentForcedReturnWeaknessRaidExempt(ordinary, clearedBinding); exempt {
t.Fatalf("普通副本中途退出被豁免了:rule=%q", rule)
}
// 4) 空值安全。
if _, exempt := service.currentForcedReturnWeaknessRaidExempt(nil, nil); exempt {
t.Fatal("nil session 被放行了")
}
if _, exempt := service.currentForcedReturnWeaknessRaidExempt(ordinary, nil); exempt {
t.Fatal("nil runtime + 普通频道被放行了")
}
}
// 上一条豁免只许落在超时空:**安徒恩(23)/卢克(32) 的频道位一个都不许被覆盖**。
//
// 超时空那条 `raid_channel_exit_never_weakens` 是"看频道类型"的(不是看绑定),所以
// 它天生比第一条宽 —— 这条用例就是它的护栏:把 `isCurrentFiendWarRaidChannel` 的
// 判据放宽成"任何攻坚频道"、或者把 42 写成 23/32,这里必须立刻变红。
//
// 两条规则的分工(都逐字保留):
//
// raid_gate_exit_never_weakens —— 看 run 上的攻坚绑定,三个团本**一视同仁**:
// 站在门里退出来本来就不该虚弱,这是既有行为。
// raid_channel_exit_never_weakens —— 只看超时空频道位;绑定已被服务器清空时兜底。
func TestForcedReturnWeaknessExemptionNeverCoversAntonOrLukeChannels(t *testing.T) {
service := &Service{}
anton := &gameSession{selectedCharacterID: 1,
channel: channelcatalog.Channel{Type: dnfenum.RaidChannelType}}
luke := &gameSession{selectedCharacterID: 1,
channel: channelcatalog.Channel{Type: dnfenum.HiddenRaidType}}
if dnfenum.RaidChannelType == dnfenum.FiendWarRaidChannelType ||
dnfenum.HiddenRaidType == dnfenum.FiendWarRaidChannelType {
t.Fatalf("频道位常量撞车:安东 %d / 卢克 %d / 超时空 %d",
dnfenum.RaidChannelType, dnfenum.HiddenRaidType, dnfenum.FiendWarRaidChannelType)
}
// 没有攻坚绑定的中途退出(= 普通副本那一类):安东/卢克必须**照旧虚弱**。
// 超时空在这一格是豁免的(上一条用例第 2 段),差别只能来自频道类型。
clearedBinding := &runtimeDungeonState{}
for name, session := range map[string]*gameSession{"安徒恩": anton, "卢克": luke} {
rule, exempt := service.currentForcedReturnWeaknessRaidExempt(session, clearedBinding)
if exempt {
t.Fatalf("%s 频道(type %d)的中途退出被超时空那条规则豁免了:rule=%q —— "+
"`raid_channel_exit_never_weakens` 只能认超时空的 %d",
name, session.channel.Type, rule, dnfenum.FiendWarRaidChannelType)
}
}
// 走门(run 上还带攻坚绑定)在三个团本上都是一样的:既有行为,逐字保留。
raidRun := &runtimeDungeonState{raidDungeon: runtimeRaidDungeonBinding{RaidKey: 12058625}}
for name, session := range map[string]*gameSession{"安徒恩": anton, "卢克": luke} {
rule, exempt := service.currentForcedReturnWeaknessRaidExempt(session, raidRun)
if !exempt || rule != "raid_gate_exit_never_weakens" {
t.Fatalf("%s 带着攻坚绑定的退出被判成要虚弱:rule=%q exempt=%t", name, rule, exempt)
}
}
}
// 「中途放弃不得虚弱」的**端到端**护栏:真走一遍 C2S op42(handleDungeonGiveupGame),
// 按频道位对比同一条路上会不会挂上虚弱。
//
// 超时空的豁免是 `raid_channel_exit_never_weakens`(只看频道类型 = 42),所以必须证明
// 它**没有**顺带把安徒恩(23)/卢克(32) 也豁免掉 —— 那才是"改了超时空别动其他团本"。
// 两条对照都要求 client_condition 那一步照旧出现(回城包序里的 op33weakness),
// 而超时空那一条必须一个字节都不发。
func TestOp42GiveUpNeverWeakensOnTheFiendWarChannel(t *testing.T) {
for _, fixture := range []struct {
name string
channelType uint8
wantWeakness bool
}{
{"超时空频道 type 42", dnfenum.FiendWarRaidChannelType, false},
{"安徒恩频道 type 23(对照)", dnfenum.RaidChannelType, true},
{"卢克频道 type 32(对照)", dnfenum.HiddenRaidType, true},
} {
t.Run(fixture.name, func(t *testing.T) {
service, session, runtime := newBackToVillageRuntime(t)
if runtime == nil {
t.Fatal("夹具没有造出一次在跑的副本")
}
session.channel = channelcatalog.Channel{ID: 184, Type: fixture.channelType}
if err := service.handleDungeonGiveupGame(session, nil); err != nil {
t.Fatalf("op42 放弃: %v", err)
}
signatures := townPostTransitionPacketSignatures(t, session.conn.(*bufferConn).write.Bytes())
shipped := slices.Contains(signatures, "op33weakness")
if shipped != fixture.wantWeakness {
t.Fatalf("op42 之后的回城包序 = %v(op33weakness=%t, want %t)",
signatures, shipped, fixture.wantWeakness)
}
repositories, ok := service.repositoryGroup()
if !ok || repositories.Character == nil {
t.Fatal("角色仓库不可用")
}
character, found, err := repositories.Character.Load(context.Background(), "99")
if err != nil || !found {
t.Fatalf("读角色 found=%t err=%v", found, err)
}
level := character.Stats[currentWeaknessStatKey]
if fixture.wantWeakness && level == 0 {
t.Fatalf("对照频道(type %d)没有挂上虚弱:stamina=%d —— 豁免漏到别的团本了",
fixture.channelType, level)
}
if !fixture.wantWeakness && level != 0 {
t.Fatalf("超时空中途放弃被挂上了虚弱 stamina=%d —— "+
"操作员裁定「攻坚战无论怎么中途放弃 都不应该虚弱」", level)
}
})
}
}