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,285 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
type vendingMachinePVFTestSource map[string]string
|
||||
|
||||
func (s vendingMachinePVFTestSource) ReadText(relativePath string) (string, error) {
|
||||
text, ok := s[relativePath]
|
||||
if !ok {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func TestCurrentVendingMachineCatalogParsesRelativeWeightsAndDummy(t *testing.T) {
|
||||
source := vendingMachinePVFTestSource{
|
||||
currentVendingMachineListPVFPath: "1 `normal.vm` 3 `high.vm` 5 `dummy.vm`",
|
||||
"etc/normal.vm": `[item group]
|
||||
[group num]
|
||||
1
|
||||
[material]
|
||||
7454 1
|
||||
[output]
|
||||
10007367 20 1 0
|
||||
7451 0 1 1
|
||||
7463 100 4 0
|
||||
[/output]
|
||||
[/item group]
|
||||
[item group]
|
||||
[group num]
|
||||
2
|
||||
[material]
|
||||
7454 2
|
||||
[output]
|
||||
31 7 1 0
|
||||
[/output]
|
||||
[/item group]`,
|
||||
"etc/high.vm": `[item group]
|
||||
[group num]
|
||||
1
|
||||
[material]
|
||||
2749933 1
|
||||
[output]
|
||||
2749957 1 1 0
|
||||
7464 2500000 10 0
|
||||
[/output]
|
||||
[/item group]`,
|
||||
"etc/dummy.vm": "[item group]\n[/item group]",
|
||||
}
|
||||
catalog, err := newCurrentVendingMachineCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := catalog.MachineCount(); got != 2 {
|
||||
t.Fatalf("machine count=%d want=2 (empty dummy must not be requestable)", got)
|
||||
}
|
||||
normal, err := catalog.Resolve(1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if normal.MaterialItemID != 7454 || normal.MaterialCount != 1 || normal.TotalWeight != 120 || len(normal.Outputs) != 3 {
|
||||
t.Fatalf("normal group=%+v", normal)
|
||||
}
|
||||
if normal.Outputs[1].Weight != 0 || normal.Outputs[1].JackpotFlag != 1 {
|
||||
t.Fatalf("zero-weight/opaque row=%+v", normal.Outputs[1])
|
||||
}
|
||||
// Resolve returns a copy so a request path cannot mutate catalog authority.
|
||||
normal.Outputs[0].ItemID = 1
|
||||
again, err := catalog.Resolve(1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again.Outputs[0].ItemID != 10007367 {
|
||||
t.Fatalf("catalog output mutated through Resolve: %+v", again.Outputs[0])
|
||||
}
|
||||
if _, err := catalog.Resolve(5, 1); !errors.Is(err, errCurrentVendingMachineGroupUnavailable) {
|
||||
t.Fatalf("dummy resolve error=%v", err)
|
||||
}
|
||||
if _, err := catalog.Resolve(1, 99); !errors.Is(err, errCurrentVendingMachineGroupUnavailable) {
|
||||
t.Fatalf("missing group error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentVendingMachineCatalogRejectsDuplicateMachineAndGroup(t *testing.T) {
|
||||
validGroup := `[item group]
|
||||
[group num]
|
||||
1
|
||||
[material]
|
||||
7454 1
|
||||
[output]
|
||||
31 1 1 0
|
||||
[/output]
|
||||
[/item group]`
|
||||
t.Run("machine", func(t *testing.T) {
|
||||
_, err := newCurrentVendingMachineCatalog(vendingMachinePVFTestSource{
|
||||
currentVendingMachineListPVFPath: "1 `a.vm` 1 `b.vm`",
|
||||
"etc/a.vm": validGroup,
|
||||
"etc/b.vm": validGroup,
|
||||
})
|
||||
if !errors.Is(err, errCurrentVendingMachineDefinitionInvalid) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
t.Run("group", func(t *testing.T) {
|
||||
_, err := newCurrentVendingMachineCatalog(vendingMachinePVFTestSource{
|
||||
currentVendingMachineListPVFPath: "1 `a.vm`",
|
||||
"etc/a.vm": validGroup + "\n" + validGroup,
|
||||
})
|
||||
if !errors.Is(err, errCurrentVendingMachineDefinitionInvalid) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCurrentVendingMachineCatalogRejectsMalformedAuthority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
definition string
|
||||
}{
|
||||
{name: "group id", definition: vendingMachineDefinitionForTest("0", "7454 1", "31 1 1 0")},
|
||||
{name: "material id", definition: vendingMachineDefinitionForTest("1", "0 1", "31 1 1 0")},
|
||||
{name: "material count", definition: vendingMachineDefinitionForTest("1", "7454 0", "31 1 1 0")},
|
||||
{name: "output id", definition: vendingMachineDefinitionForTest("1", "7454 1", "0 1 1 0")},
|
||||
{name: "negative weight", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 -1 1 0")},
|
||||
{name: "zero count", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 1 0 0")},
|
||||
{name: "flag", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 1 1 2")},
|
||||
{name: "no positive weight", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 0 1 0")},
|
||||
{name: "output arity", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 1 1")},
|
||||
{name: "non integer", definition: vendingMachineDefinitionForTest("1", "7454 1", "31 1.5 1 0")},
|
||||
{name: "partial dummy", definition: "[item group]\n[group num]\n1\n[/item group]"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := newCurrentVendingMachineCatalog(vendingMachinePVFTestSource{
|
||||
currentVendingMachineListPVFPath: "1 `bad.vm`",
|
||||
"etc/bad.vm": test.definition,
|
||||
})
|
||||
if !errors.Is(err, errCurrentVendingMachineDefinitionInvalid) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentVendingMachineCatalogFailsClosedOnSourceErrors(t *testing.T) {
|
||||
if _, err := newCurrentVendingMachineCatalog(nil); !errors.Is(err, errCurrentVendingMachineCatalogUnavailable) {
|
||||
t.Fatalf("nil source error=%v", err)
|
||||
}
|
||||
_, err := newCurrentVendingMachineCatalog(vendingMachinePVFTestSource{
|
||||
currentVendingMachineListPVFPath: "1 `missing.vm`",
|
||||
})
|
||||
if !errors.Is(err, errCurrentVendingMachineCatalogUnavailable) {
|
||||
t.Fatalf("missing definition error=%v", err)
|
||||
}
|
||||
var catalog *currentVendingMachineCatalog
|
||||
if _, err := catalog.Resolve(1, 1); !errors.Is(err, errCurrentVendingMachineCatalogUnavailable) {
|
||||
t.Fatalf("nil catalog resolve error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func vendingMachineDefinitionForTest(groupNum, material, output string) string {
|
||||
return "[item group]\n[group num]\n" + groupNum + "\n[material]\n" + material + "\n[output]\n" + output + "\n[/output]\n[/item group]"
|
||||
}
|
||||
|
||||
func TestCurrentVendingMachineCatalogRealScriptPVF(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
pvfPath = filepath.Clean(filepath.Join("..", "..", "..", "..", "runtime", "data", "dnf", "Script.pvf"))
|
||||
if _, err := os.Stat(pvfPath); err != nil {
|
||||
t.Skipf("real Script.pvf unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open real Script.pvf: %v", err)
|
||||
}
|
||||
catalog, err := newCurrentVendingMachineCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("load real vending-machine catalog: %v", err)
|
||||
}
|
||||
|
||||
normal, err := catalog.Resolve(1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if normal.MaterialItemID != 7454 || normal.MaterialCount != 1 {
|
||||
t.Fatalf("machine 1 material=%d x%d", normal.MaterialItemID, normal.MaterialCount)
|
||||
}
|
||||
if !currentVendingMachineOutputMatches(normal, 10007367, 20000, 1) {
|
||||
t.Fatalf("machine 1 does not contain Lucky Hammer output: %+v", normal.Outputs)
|
||||
}
|
||||
assertCurrentVendingMachineRelativeTotal(t, normal)
|
||||
|
||||
high, err := catalog.Resolve(3, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if high.MaterialItemID != 2749933 || high.MaterialCount != 1 {
|
||||
t.Fatalf("machine 3 material=%d x%d", high.MaterialItemID, high.MaterialCount)
|
||||
}
|
||||
if !currentVendingMachineOutputMatches(high, 2749957, 1, 1) {
|
||||
t.Fatalf("machine 3 does not contain its weight-1 high-prize output: %+v", high.Outputs)
|
||||
}
|
||||
assertCurrentVendingMachineRelativeTotal(t, high)
|
||||
|
||||
if _, err := catalog.Resolve(5, 1); !errors.Is(err, errCurrentVendingMachineGroupUnavailable) {
|
||||
t.Fatalf("real dummy machine is requestable: %v", err)
|
||||
}
|
||||
|
||||
itemCatalog, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("load real item catalog: %v", err)
|
||||
}
|
||||
for _, group := range []currentVendingMachineGroup{normal, high} {
|
||||
for _, output := range group.Outputs {
|
||||
if output.Weight == 0 {
|
||||
continue
|
||||
}
|
||||
definition, resolveErr := itemCatalog.ResolveItem(output.ItemID)
|
||||
if resolveErr != nil {
|
||||
t.Fatalf("machine=%d unresolved output=%d: %v", group.MachineID, output.ItemID, resolveErr)
|
||||
}
|
||||
if isCurrentCeraShopCreatureItem(definition) || isCurrentCeraShopPetConsumable(definition) {
|
||||
t.Fatalf("machine=%d output=%d resolves outside the op215 main-inventory row contract", group.MachineID, output.ItemID)
|
||||
}
|
||||
candidate := dnfrepo.InventoryRecord{CharacterID: "smoke", Slots: make(map[string]dnfrepo.ItemStack)}
|
||||
touched, grantErr := grantCurrentCeraShopProduct(&candidate, definition, output.Count, 24)
|
||||
if grantErr != nil {
|
||||
t.Fatalf("machine=%d output=%d count=%d cannot be projected: %v", group.MachineID, output.ItemID, output.Count, grantErr)
|
||||
}
|
||||
rows := make([]currentVendingMachineRewardRow, 0, len(touched))
|
||||
for _, slot := range touched {
|
||||
if slot > 32767 {
|
||||
t.Fatalf("machine=%d output=%d projected slot out of wire range=%d", group.MachineID, output.ItemID, slot)
|
||||
}
|
||||
signedSlot := int16(slot)
|
||||
stack, found := candidate.Slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, signedSlot)]
|
||||
if !found {
|
||||
t.Fatalf("machine=%d output=%d missing projected slot=%d", group.MachineID, output.ItemID, slot)
|
||||
}
|
||||
rows = append(rows, currentVendingMachineRewardRow{
|
||||
ListType: dnfrepo.MainInventoryListType,
|
||||
Entry: currentItemListEntryFromStack(dnfrepo.MainInventoryListType, signedSlot, stack),
|
||||
})
|
||||
}
|
||||
if _, buildErr := buildCurrentVendingMachineSuccessBody(currentVendingMachineSuccess{
|
||||
MaterialSlot: 140,
|
||||
MaterialRemaining: 1,
|
||||
RewardItemID: output.ItemID,
|
||||
RewardCount: output.Count,
|
||||
RewardRows: rows,
|
||||
}); buildErr != nil {
|
||||
t.Fatalf("machine=%d output=%d count=%d cannot build native op215 rows: %v", group.MachineID, output.ItemID, output.Count, buildErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func currentVendingMachineOutputMatches(group currentVendingMachineGroup, itemID uint32, weight uint64, count uint32) bool {
|
||||
for _, output := range group.Outputs {
|
||||
if output.ItemID == itemID && output.Weight == weight && output.Count == count {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func assertCurrentVendingMachineRelativeTotal(t *testing.T, group currentVendingMachineGroup) {
|
||||
t.Helper()
|
||||
var sum uint64
|
||||
for _, output := range group.Outputs {
|
||||
sum += output.Weight
|
||||
}
|
||||
if sum == 0 || group.TotalWeight != sum {
|
||||
t.Fatalf("machine=%d group=%d total=%d summed relative weights=%d", group.MachineID, group.GroupID, group.TotalWeight, sum)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user