Files
90CNServer/server/internal/modules/dnf/creaturestate/egg_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

133 lines
5.1 KiB
Go

package creaturestate
import (
"context"
"testing"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
)
// 线上 Script.pvf 里 2681466 = `equipment/creature/egg_SDCharacterCreature_ft.equ`
// (宠物蛋 (迷你气功师),带 [output index] 2681467);
// 400990167 / 400990168 是直接发放的真宠物(无 [output index])。
const (
testPetEggItemID int64 = 2681466
testRealPetItemID int64 = 400990167
testPetEggSerial uint32 = 2
)
// TestReconcileHealsPetEggBinding 覆盖现场:蛋被当成"直接发放的宠物",拿到了
// serial 并建出了宠物记录,于是丢弃侧按"已绑定宠物"拒绝(「宠物蛋无法丢弃」)。
// 对账必须自愈:清掉行上的绑定 extra、删掉误建的宠物记录。
func TestReconcileHealsPetEggBinding(t *testing.T) {
ctx := context.Background()
const characterID = "77"
SetPetEggProbe(func(itemID int64) bool { return itemID == testPetEggItemID })
defer SetPetEggProbe(nil)
repos := dnfrepomemory.NewMemoryGroup()
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: characterID, Slots: map[string]dnfrepo.ItemStack{
"7:0": {ItemID: testPetEggItemID, Count: 1, Extra: map[string]string{
"creature_serial_or_handle": "2",
"creature_key": "2",
"item_kind": "equipment",
"equipment_type": "[creature]",
"pvf_path": "equipment/creature/egg_SDCharacterCreature_ft.equ",
}},
"7:1": {ItemID: testRealPetItemID, Count: 1, Extra: map[string]string{
"creature_serial_or_handle": "1",
"item_kind": "equipment",
"equipment_type": "[creature]",
"pvf_path": "equipment/creature/chn_2018_SpringFestival/chn_400990167.equ",
}},
}}); err != nil {
t.Fatal(err)
}
if err := repos.Equipment.Save(ctx, dnfrepo.EquipmentRecord{CharacterID: characterID, Entries: map[string]dnfrepo.EquipmentEntry{}}); err != nil {
t.Fatal(err)
}
if err := repos.Pet.Save(ctx, dnfrepo.PetRecord{CharacterID: characterID, Entries: map[string]dnfrepo.PetEntry{
"2": {PetKey: "2", CreatureKey: 2, ItemID: testPetEggItemID, SourceListType: 7, SourceSlotIndex: 0},
"1": {PetKey: "1", CreatureKey: 1, ItemID: testRealPetItemID, SourceListType: 7, SourceSlotIndex: 1},
}}); err != nil {
t.Fatal(err)
}
if _, err := ReconcileInventory(ctx, characterID, repos.Inventory, repos.Equipment, repos.Pet); err != nil {
t.Fatalf("ReconcileInventory: %v", err)
}
inventory, found, err := repos.Inventory.Load(ctx, characterID)
if err != nil || !found {
t.Fatalf("load inventory found=%t err=%v", found, err)
}
egg := inventory.Slots["7:0"]
if !stackLacksPetBinding(egg) {
t.Fatalf("蛋行仍带宠物绑定 extra: %+v", egg.Extra)
}
petRecord, found, err := repos.Pet.Load(ctx, characterID)
if err != nil || !found {
t.Fatalf("load pets found=%t err=%v", found, err)
}
if _, still := petRecord.Entries["2"]; still {
t.Fatalf("蛋的宠物记录没有被清理: %+v", petRecord.Entries)
}
// 真宠物必须不受影响。
if _, ok := petRecord.Entries["1"]; !ok {
t.Fatalf("真宠物的记录被误删: %+v", petRecord.Entries)
}
if got := inventory.Slots["7:1"].Extra["creature_serial_or_handle"]; got != "1" {
t.Fatalf("真宠物的 serial 被误改: %q", got)
}
}
// TestReconcileKeepsEggBehaviourWithoutProbe 钉住回退路径:判据未装配(隔离测试、
// 未接线的调用方)时必须完全等于改动前的行为。
func TestReconcileKeepsEggBehaviourWithoutProbe(t *testing.T) {
ctx := context.Background()
const characterID = "78"
SetPetEggProbe(nil)
defer SetPetEggProbe(nil)
repos := dnfrepomemory.NewMemoryGroup()
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: characterID, Slots: map[string]dnfrepo.ItemStack{
"7:0": {ItemID: testPetEggItemID, Count: 1, Extra: map[string]string{
"item_kind": "equipment",
"equipment_type": "[creature]",
"pvf_path": "equipment/creature/egg_SDCharacterCreature_ft.equ",
}},
}}); err != nil {
t.Fatal(err)
}
if err := repos.Equipment.Save(ctx, dnfrepo.EquipmentRecord{CharacterID: characterID, Entries: map[string]dnfrepo.EquipmentEntry{}}); err != nil {
t.Fatal(err)
}
if err := repos.Pet.Save(ctx, dnfrepo.PetRecord{CharacterID: characterID, Entries: map[string]dnfrepo.PetEntry{}}); err != nil {
t.Fatal(err)
}
if _, err := ReconcileInventory(ctx, characterID, repos.Inventory, repos.Equipment, repos.Pet); err != nil {
t.Fatalf("ReconcileInventory: %v", err)
}
inventory, _, _ := repos.Inventory.Load(ctx, characterID)
if got := inventory.Slots["7:0"].Extra["creature_serial_or_handle"]; got == "" {
t.Fatal("未装配判据时不应改变行为(旧行为会给蛋分配 serial)")
}
}
func stackLacksPetBinding(stack dnfrepo.ItemStack) bool {
if _, ok := stackSerial(stack.Extra); ok {
return false
}
for _, key := range []string{"creature_serial_or_handle", "creature_key", "creature_serial", "pet_serial"} {
if stack.Extra[key] != "" {
return false
}
}
return true
}