服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
148 lines
5.7 KiB
Go
148 lines
5.7 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"encoding/hex"
|
||
"os"
|
||
"strconv"
|
||
"testing"
|
||
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||
)
|
||
|
||
// TestCurrentAuraLinkedSocketBlockResolvesLiveInstance 钉住"镶嵌属性靠实时读背包"
|
||
// 这条链路:关联块的内容必须来自被覆盖光环**当前所在的背包行**,而不是穿戴时的
|
||
// 快照(玩家可能在穿好转职光环之后才打徽章)。绑定失效时必须什么都不发。
|
||
func TestCurrentAuraLinkedSocketBlockResolvesLiveInstance(t *testing.T) {
|
||
const (
|
||
coveredAura int64 = 101590068
|
||
otherAura int64 = 101590051
|
||
)
|
||
sockets := make([]byte, currentAvatarSocketBytes)
|
||
for i := range sockets {
|
||
sockets[i] = byte(0x10 + i)
|
||
}
|
||
hexSockets := hex.EncodeToString(sockets)
|
||
|
||
extra := map[string]string{
|
||
dnfrepo.AuraLinkedTemplateExtraKey: strconv.FormatInt(coveredAura, 10),
|
||
dnfrepo.AuraLinkedSourceExtraKey: "1:31",
|
||
}
|
||
slots := map[string]dnfrepo.ItemStack{
|
||
"1:31": {ItemID: coveredAura, Extra: map[string]string{"avatar_socket_data": hexSockets}},
|
||
}
|
||
|
||
if got := currentAuraLinkedSocketBlock(extra, slots); len(got) == 0 {
|
||
t.Fatal("live inventory row must yield the covered aura socket block")
|
||
}
|
||
|
||
// 绑定失效的若干情形都必须 fail closed(不发块,也绝不改猜别的光环)。
|
||
cases := []struct {
|
||
name string
|
||
ex map[string]string
|
||
sl map[string]dnfrepo.ItemStack
|
||
}{
|
||
{"no inventory", extra, nil},
|
||
{"empty inventory", extra, map[string]dnfrepo.ItemStack{}},
|
||
{"source slot missing", extra, map[string]dnfrepo.ItemStack{"1:9": {ItemID: coveredAura}}},
|
||
{"source slot holds another item", extra, map[string]dnfrepo.ItemStack{"1:31": {ItemID: otherAura}}},
|
||
{"no source recorded", map[string]string{dnfrepo.AuraLinkedTemplateExtraKey: strconv.FormatInt(coveredAura, 10)}, slots},
|
||
{"no template recorded", map[string]string{dnfrepo.AuraLinkedSourceExtraKey: "1:31"}, slots},
|
||
{"clone avatar key only", map[string]string{dnfrepo.CloneAvatarAppearanceExtraKey: "415500058"}, slots},
|
||
}
|
||
for _, c := range cases {
|
||
if got := currentAuraLinkedSocketBlock(c.ex, c.sl); got != nil {
|
||
t.Fatalf("%s: expected no block, got %d bytes", c.name, len(got))
|
||
}
|
||
}
|
||
|
||
// 显式捕获的别名优先,保持改动前的行为。
|
||
if got := currentMode1EquipmentLinkedRaw(
|
||
map[string]string{"mode1_linked_raw_hex": "aabb"}, slots); len(got) != 2 {
|
||
t.Fatalf("explicit mode1_linked_raw_hex must win, got %d bytes", len(got))
|
||
}
|
||
}
|
||
|
||
// TestCurrentActorAppearanceItemIDIgnoresAuraLink 是这次修复的直接护栏:
|
||
// 转职光环把自己的 item id 作为外观下发,只有被覆盖光环的关联模板写在另一个
|
||
// key 上;克隆装扮的借用行为必须完全不变。
|
||
func TestCurrentActorAppearanceItemIDIgnoresAuraLink(t *testing.T) {
|
||
const (
|
||
aura int64 = 113590008 // 转职光环
|
||
coveredAura int64 = 101590068 // 被它覆盖的普通光环
|
||
)
|
||
// 槽 9 的转职光环:带关联模板,但外观必须是它自己。旧代码把关联写进
|
||
// clone_avatar_appearance_id,于是这里会返回 101590068,客户端渲染回原光环。
|
||
equipped := dnfrepo.EquipmentEntry{
|
||
SlotIndex: 9,
|
||
ItemID: aura,
|
||
Extra: map[string]string{
|
||
"current_exe_equipment_type": "9",
|
||
dnfrepo.AuraLinkedTemplateExtraKey: "101590068",
|
||
},
|
||
}
|
||
if got := currentActorAppearanceItemID(equipped); got != uint32(aura) {
|
||
t.Fatalf("aura appearance item=%d want=%d (转职光环 must render itself)", got, aura)
|
||
}
|
||
|
||
// 对照组:透明克隆时装仍然借用被克隆模板。
|
||
clone := dnfrepo.EquipmentEntry{
|
||
SlotIndex: 3,
|
||
ItemID: 415500017,
|
||
Extra: map[string]string{
|
||
"current_exe_equipment_type": "3",
|
||
dnfrepo.CloneAvatarAppearanceExtraKey: "415500058",
|
||
},
|
||
}
|
||
if got := currentActorAppearanceItemID(clone); got != 415500058 {
|
||
t.Fatalf("clone appearance item=%d want=415500058", got)
|
||
}
|
||
|
||
// 两种 key 同时存在(不应发生)时,关联 key 也不能影响外观。
|
||
both := dnfrepo.EquipmentEntry{
|
||
SlotIndex: 3,
|
||
ItemID: 415500017,
|
||
Extra: map[string]string{
|
||
"current_exe_equipment_type": "3",
|
||
dnfrepo.AuraLinkedTemplateExtraKey: "101590068",
|
||
dnfrepo.CloneAvatarAppearanceExtraKey: "415500058",
|
||
},
|
||
}
|
||
if got := currentActorAppearanceItemID(both); got != 415500058 {
|
||
t.Fatalf("appearance with both keys=%d want=415500058", got)
|
||
}
|
||
}
|
||
|
||
// TestCurrentPVFClassifiesAuroraLookReplaceAvatar 用真实 Script.pvf 钉住判据:
|
||
// 全库只有 113590006 / 113590008 / 113590009 同时满足
|
||
// `[equipment type] [aurora avatar]` 与 `[item category] clear avatar`,
|
||
// 并且它们仍然是 clear avatar(分类契约不能被改动,否则克隆路径会漂移)。
|
||
func TestCurrentPVFClassifiesAuroraLookReplaceAvatar(t *testing.T) {
|
||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||
if pvfPath == "" {
|
||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to run real Script.pvf aura smoke")
|
||
}
|
||
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
service := &Service{initialEquipmentArchive: archive}
|
||
|
||
for _, itemID := range []uint32{113590006, 113590008, 113590009} {
|
||
if !service.currentIsAuroraLookReplaceAvatar(itemID) {
|
||
t.Fatalf("item %d must classify as 转职光环", itemID)
|
||
}
|
||
if isAvatar, isClone := service.currentClassifyAvatarItem(itemID); !isAvatar || !isClone {
|
||
t.Fatalf("item %d classification avatar=%t clone=%t (must stay clear avatar)",
|
||
itemID, isAvatar, isClone)
|
||
}
|
||
}
|
||
|
||
// 普通光环、克隆帽、普通装扮、普通装备都必须返回 false。
|
||
for _, itemID := range []uint32{101590068, 415500017, 415500058, 27000} {
|
||
if service.currentIsAuroraLookReplaceAvatar(itemID) {
|
||
t.Fatalf("item %d must not classify as 转职光环", itemID)
|
||
}
|
||
}
|
||
}
|