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,218 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
)
|
||||
|
||||
func shopFixture(t *testing.T) (*core, int32) {
|
||||
t.Helper()
|
||||
a := pvf.New()
|
||||
for p, text := range map[string]string{
|
||||
shopCategoryPath: "[itemshop category]\n[category name]\n`basic job`\n[category entry]\n[id]\n0\n[text]\n`男鬼剑士`\n[/category entry]\n[category entry]\n[id]\n1\n[text]\n`女格斗家`\n[/category entry]\n[/itemshop category]\n[itemshop category]\n[category name]\n`expert job`\n[category entry]\n[id]\n0\n[text]\n`炼金术师`\n[/category entry]\n[category entry]\n[id]\n1\n[text]\n`附魔师`\n[/category entry]\n[category entry]\n[id]\n2\n[text]\n`控偶师`\n[/category entry]\n[category entry]\n[id]\n3\n[text]\n`分解师`\n[/category entry]\n[/itemshop category]\n[itemshop category]\n[category name]\n`expert job non filter`\n[category entry]\n[id]\n0\n[text]\n`炼金术师`\n[/category entry]\n[category entry]\n[id]\n1\n[text]\n`附魔师`\n[/category entry]\n[category entry]\n[id]\n2\n[text]\n`控偶师`\n[/category entry]\n[category entry]\n[id]\n3\n[text]\n`分解师`\n[/category entry]\n[/itemshop category]",
|
||||
"stackable/stackable.lst": "1 `one.stk` 2 `two.stk` 3 `three.stk`",
|
||||
"stackable/one.stk": "[name]\n`长名称药剂`\n[icon]\n`item/test.img` 7\n[price]\n100000\n[need material]\n2 10 3 20\n[/need material]",
|
||||
"stackable/two.stk": "[name]\n`材料甲`\n[icon]\n`item/test.img` 8",
|
||||
"stackable/three.stk": "[name]\n`材料乙`",
|
||||
"equipment/equipment.lst": "4 `four.equ`",
|
||||
"equipment/four.equ": "[name]\n`佩刀`\n[price]\n3500",
|
||||
"character/character.lst": "0 `sword.chr` 1 `fighter.chr`",
|
||||
"character/sword.chr": "[name]\n`不能用此名称`\n[growtype name]\n`鬼剑士` `剑魂`",
|
||||
"character/fighter.chr": "[name]\n`格斗家`",
|
||||
"npc/npc.lst": "10 `shop.npc`",
|
||||
"npc/shop.npc": "[name]\n`商人`",
|
||||
} {
|
||||
if _, err := a.AddFileText(p, text, pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
i, err := a.AddFileText("itemshop/test.shp", "[NPC]\n10", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := NewCore()
|
||||
c.archive = a
|
||||
return c, i
|
||||
}
|
||||
|
||||
func TestShopDraftCostsAndReadOnly(t *testing.T) {
|
||||
c, i := shopFixture(t)
|
||||
before, _ := c.archive.RawBytes(i)
|
||||
saved := append([]byte(nil), before...)
|
||||
modified := c.archive.ModifiedCount()
|
||||
text := "[NPC]\n10\n[sell info]\n[tab]\n`珍贵😀`\n[item list]\n1 1 2 4 999\n[/item list]\n[/tab]\n[tab]\n`空`\n[item list]\n[/item list]\n[/tab]\n[/sell info]"
|
||||
doc, err := NewFileGUIService(c).ReadShop(i, text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc.Name != "商人" || len(doc.Tabs) != 2 || len(doc.Categories) != 0 {
|
||||
t.Fatalf("document = %#v", doc)
|
||||
}
|
||||
entries := doc.Tabs[0].Groups[0].Items
|
||||
if len(entries) != 5 || entries[0].Item.ID != entries[1].Item.ID {
|
||||
t.Fatalf("entries = %#v", entries)
|
||||
}
|
||||
item := entries[0].Item
|
||||
if item.Name != "长名称药剂" || item.Icon == nil || item.Icon.Index != 7 || len(item.Costs) != 3 {
|
||||
t.Fatalf("item = %#v", item)
|
||||
}
|
||||
if item.Costs[0].Kind != "gold" || item.Costs[0].Quantity != "100000" || item.Costs[1].ItemID != "2" || item.Costs[1].Quantity != "10" || item.Costs[1].Name != "材料甲" || item.Costs[1].Icon == nil {
|
||||
t.Fatalf("costs = %#v", item.Costs)
|
||||
}
|
||||
if len(entries[2].Item.Costs) != 0 || entries[3].Item.Costs[0].Quantity != "3500" || !strings.Contains(entries[4].Item.Name, "999") || len(doc.Issues) != 1 {
|
||||
t.Fatal("missing price / equipment / unknown item handling")
|
||||
}
|
||||
runes := utf16.Encode([]rune(text))
|
||||
for _, entry := range entries {
|
||||
if string(utf16.Decode(runes[entry.SourceStart:entry.SourceEnd])) != entry.Item.ID {
|
||||
t.Fatal("source range mismatch")
|
||||
}
|
||||
}
|
||||
after, _ := c.archive.RawBytes(i)
|
||||
if string(after) != string(saved) || c.archive.ModifiedCount() != modified {
|
||||
t.Fatal("GUI read mutated archive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopCategoryKinds(t *testing.T) {
|
||||
c, i := shopFixture(t)
|
||||
for _, tc := range []struct{ kind, id, want string }{
|
||||
{"basic job", "0", "男鬼剑士"}, {"basic job", "1", "女格斗家"}, {"expert job", "1", "附魔师"}, {"expert job", "0", "炼金术师"}, {"expert job", "3", "分解师"}, {"expert job", "2", "控偶师"},
|
||||
{"expert job non filter", "1", "附魔师"}, {"expert job non filter", "0", "炼金术师"}, {"expert job non filter", "3", "分解师"}, {"expert job non filter", "2", "控偶师"},
|
||||
} {
|
||||
t.Run(tc.kind+tc.id, func(t *testing.T) {
|
||||
text := "[sell info]\n[use category]\n`" + tc.kind + "`\n[tab]\n`分类商品`\n[category entry]\n[id]\n" + tc.id + "\n[item list]\n1 2\n[/item list]\n[/category entry]\n[/tab]\n[/sell info]"
|
||||
doc, err := NewFileGUIService(c).ReadShop(i, text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := ""
|
||||
for _, category := range doc.Categories {
|
||||
if category.ID == tc.id {
|
||||
name = category.Name
|
||||
}
|
||||
}
|
||||
if name != tc.want || doc.Tabs[0].Groups[0].CategoryID != tc.id || len(doc.Tabs[0].Groups[0].Items) != 2 {
|
||||
t.Fatalf("document = %#v", doc)
|
||||
}
|
||||
if doc.CategoryType != tc.kind || len(doc.Issues) != 0 {
|
||||
t.Fatalf("category type = %q, issues = %#v", doc.CategoryType, doc.Issues)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopMalformedAndEmpty(t *testing.T) {
|
||||
for _, text := range []string{"", "[item list]\n1\n[/item list]", "[sell info]\n[tab]\n`bad`\n[item list]\n`not an id`\n[/item list]\n[/tab]\n[/sell info]"} {
|
||||
if len(parseShop(text).Issues) == 0 {
|
||||
t.Fatalf("missing issue for %q", text)
|
||||
}
|
||||
}
|
||||
doc := parseShop("")
|
||||
costs := shopCosts("[price]\n-1\n[need material]\n2 10 3\n[/need material]", "1", doc)
|
||||
if len(costs) != 1 || costs[0].ItemID != "2" || len(doc.Issues) < 3 {
|
||||
t.Fatalf("costs=%#v issues=%#v", costs, doc.Issues)
|
||||
}
|
||||
doc = parseShop("")
|
||||
if costs := shopCosts("[price]\n0", "1", doc); len(costs) != 1 || costs[0].Quantity != "0" {
|
||||
t.Fatal("explicit zero price must be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopReal90CN(t *testing.T) {
|
||||
filename := os.Getenv("PVF_TESTFILE")
|
||||
if filename == "" {
|
||||
t.Skip("PVF_TESTFILE 未设置")
|
||||
}
|
||||
a, err := pvf.Open(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.ClientVersion() != "90CN" {
|
||||
t.Skip("商店样例使用 90CN")
|
||||
}
|
||||
c := NewCore()
|
||||
c.archive = a
|
||||
service := NewFileGUIService(c)
|
||||
before := a.ModifiedCount()
|
||||
for _, p := range []string{"itemshop/101_joann_box.shp", "itemshop/equipmentshop1.shp"} {
|
||||
i, ok := a.Find(p)
|
||||
if !ok {
|
||||
t.Fatal("sample missing")
|
||||
}
|
||||
text, err := a.Text(i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc, err := service.ReadShop(i, text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(p, "joann") {
|
||||
if len(doc.Tabs) != 3 || len(doc.Categories) != 0 {
|
||||
t.Fatal("joann structure")
|
||||
}
|
||||
for ti, count := range []int{5, 6, 6} {
|
||||
if len(doc.Tabs[ti].Groups[0].Items) != count {
|
||||
t.Fatal("joann item count")
|
||||
}
|
||||
}
|
||||
cost := doc.Tabs[0].Groups[0].Items[0].Item.Costs
|
||||
if len(cost) != 1 || cost[0].ItemID != "10092849" || cost[0].Quantity != "10" {
|
||||
t.Fatalf("joann cost=%#v", cost)
|
||||
}
|
||||
} else {
|
||||
if len(doc.Tabs) != 4 || len(doc.Categories) != 16 || doc.Categories[0].Name != "鬼剑士(男)" {
|
||||
t.Fatalf("equipment categories=%#v tabs=%d", doc.Categories, len(doc.Tabs))
|
||||
}
|
||||
if doc.Tabs[0].Groups[0].Items[0].Item.Costs[0].Quantity != "10" {
|
||||
t.Fatal("equipment gold price")
|
||||
}
|
||||
}
|
||||
if len(doc.Issues) > 0 {
|
||||
t.Fatalf("sample has issues: %#v", doc.Issues)
|
||||
}
|
||||
}
|
||||
if a.ModifiedCount() != before {
|
||||
t.Fatal("real archive mutated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopCategoryRealArchive(t *testing.T) {
|
||||
filename := os.Getenv("PVF_TESTFILE")
|
||||
if filename == "" {
|
||||
t.Skip("PVF_TESTFILE 未设置")
|
||||
}
|
||||
a, err := pvf.Open(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
kind string
|
||||
firstID string
|
||||
count int
|
||||
}{
|
||||
{"job", "0", 0},
|
||||
{"expert job", "0", 4},
|
||||
{"expert job non filter", "0", 4},
|
||||
{"basic job", "0", 16},
|
||||
} {
|
||||
categories, err := readShopCategories(a, tc.kind)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(categories) < tc.count || categories[0].ID != tc.firstID || categories[0].Name == "" || strings.Contains(categories[0].Name, "<4::") {
|
||||
t.Fatalf("%s categories = %#v", tc.kind, categories)
|
||||
}
|
||||
}
|
||||
if a.ClientVersion() == "110US" {
|
||||
categories, err := readShopCategories(a, "basic job")
|
||||
if err != nil || len(categories) != 17 || categories[16].ID != "16" {
|
||||
t.Fatalf("110US basic job = %#v, %v", categories, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user