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,331 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
)
|
||||
|
||||
const editableShop = "[NPC]\n10\n[sell info]\n[tab]\n`第一页`\n[item list]\n1 1 4\n[/item list]\n[/tab]\n[tab]\n`第二页`\n[item list]\n2\n[/item list]\n[/tab]\n[/sell info]\n[message]\n`保留的消息`"
|
||||
|
||||
func editFixture(t *testing.T, text string) (*core, *FileGUIService, ShopEditRequest) {
|
||||
t.Helper()
|
||||
c, i := shopFixture(t)
|
||||
if err := c.archive.SetText(i, text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.setArchive(c.archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(c.closeArchive)
|
||||
return c, NewFileGUIService(c), ShopEditRequest{FileIndex: i, Path: c.archive.Path(i), Text: text, Revision: c.batchRevision, TabIndex: 0}
|
||||
}
|
||||
func TestShopEditItemCostsAndDrafts(t *testing.T) {
|
||||
c, s, r := editFixture(t, editableShop)
|
||||
doc, err := s.ReadShop(r.FileIndex, r.Text)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Action = "edit-item"
|
||||
r.SourceStart = doc.Tabs[0].Groups[0].Items[0].SourceStart
|
||||
r.ItemID = "1"
|
||||
r.SetGold = true
|
||||
r.Gold = "123"
|
||||
r.SetMaterials = true
|
||||
r.Materials = []ShopMaterialInput{{ItemID: "3", Quantity: "7"}}
|
||||
i, _ := c.archive.Find("stackable/one.stk")
|
||||
text, err := c.archive.Text(i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Drafts = []ShopDraft{{FileIndex: i, Path: c.archive.Path(i), Text: text + "\n[explain]\n`草稿中的其他修改`"}}
|
||||
result, err := s.ApplyShopEdit(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.AffectedItems != 1 || len(result.Files) != 1 {
|
||||
t.Fatalf("result=%#v", result)
|
||||
}
|
||||
got, err := c.archive.Text(i)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if firstSectionValue(got, "price") != "123" || !strings.Contains(got, "草稿中的其他修改") {
|
||||
t.Fatalf("text=%s", got)
|
||||
}
|
||||
material := shopCosts(got, "1", &ShopDocument{})
|
||||
if len(material) != 2 || material[1].ItemID != "3" || material[1].Quantity != "7" {
|
||||
t.Fatalf("cost=%#v", material)
|
||||
}
|
||||
// Both references reflect the shared product cost, but the shop structure is unchanged.
|
||||
shop, _ := c.archive.Text(r.FileIndex)
|
||||
if !strings.Contains(shop, "保留的消息") {
|
||||
t.Fatal("unrelated shop field lost")
|
||||
}
|
||||
}
|
||||
func TestShopEditReplacesOneOccurrenceAndLeavesOldItem(t *testing.T) {
|
||||
c, s, r := editFixture(t, editableShop)
|
||||
doc, _ := s.ReadShop(r.FileIndex, r.Text)
|
||||
oldIndex, _ := c.archive.Find("stackable/one.stk")
|
||||
old, _ := c.archive.RawBytes(oldIndex)
|
||||
old = append([]byte(nil), old...)
|
||||
r.Action = "edit-item"
|
||||
r.SourceStart = doc.Tabs[0].Groups[0].Items[0].SourceStart
|
||||
r.ItemID = "4"
|
||||
r.SetGold = true
|
||||
r.Gold = "3500"
|
||||
r.SetMaterials = true
|
||||
if _, err := s.ApplyShopEdit(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text, _ := c.archive.Text(r.FileIndex)
|
||||
entries := parseShop(text).Tabs[0].Groups[0].Items
|
||||
if entries[0].Item.ID != "4" || entries[1].Item.ID != "1" {
|
||||
t.Fatal("wrong duplicate replaced")
|
||||
}
|
||||
current, _ := c.archive.RawBytes(oldIndex)
|
||||
if !bytes.Equal(old, current) {
|
||||
t.Fatal("old product mutated")
|
||||
}
|
||||
}
|
||||
func TestShopBatchAllCategoriesAtomic(t *testing.T) {
|
||||
text := "[sell info]\n[use category]\n`expert job non filter`\n[tab]\n`商品`\n[category entry]\n[id]\n1\n[item list]\n1 1\n[/item list]\n[/category entry]\n[category entry]\n[id]\n2\n[item list]\n4\n[/item list]\n[/category entry]\n[/tab]\n[/sell info]"
|
||||
c, s, r := editFixture(t, text)
|
||||
r.Action = "batch-costs"
|
||||
r.SetGold = true
|
||||
r.Gold = "500"
|
||||
r.SetMaterials = true
|
||||
r.Materials = []ShopMaterialInput{{ItemID: "999999", Quantity: "1"}}
|
||||
before := map[int32][]byte{}
|
||||
for i := int32(0); i < c.archive.FileCount(); i++ {
|
||||
raw, _ := c.archive.RawBytes(i)
|
||||
before[i] = append([]byte(nil), raw...)
|
||||
}
|
||||
if _, err := s.ApplyShopEdit(r); err == nil {
|
||||
t.Fatal("unknown material accepted")
|
||||
}
|
||||
if c.batchRevision != r.Revision {
|
||||
t.Fatal("failed edit advanced revision")
|
||||
}
|
||||
for i, old := range before {
|
||||
raw, _ := c.archive.RawBytes(i)
|
||||
if !bytes.Equal(raw, old) {
|
||||
t.Fatal("partially applied batch")
|
||||
}
|
||||
}
|
||||
r.Materials = []ShopMaterialInput{{ItemID: "2", Quantity: "3"}}
|
||||
result, err := s.ApplyShopEdit(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.AffectedItems != 2 || len(result.Files) != 2 {
|
||||
t.Fatalf("result=%#v", result)
|
||||
}
|
||||
for _, p := range []string{"stackable/one.stk", "equipment/four.equ"} {
|
||||
i, _ := c.archive.Find(p)
|
||||
text, _ := c.archive.Text(i)
|
||||
costs := shopCosts(text, p, &ShopDocument{})
|
||||
if len(costs) != 2 || costs[0].Quantity != "500" || costs[1].Quantity != "3" {
|
||||
t.Fatalf("costs=%#v", costs)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestShopTabOperationsAndDeletion(t *testing.T) {
|
||||
c, s, r := editFixture(t, editableShop)
|
||||
apply := func(action string) {
|
||||
t.Helper()
|
||||
r.Action = action
|
||||
r.Revision = c.batchRevision
|
||||
r.Text, _ = c.archive.Text(r.FileIndex)
|
||||
if _, err := s.ApplyShopEdit(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
r.Name = "重命名"
|
||||
apply("rename-tab")
|
||||
text, _ := c.archive.Text(r.FileIndex)
|
||||
if parseShop(text).Tabs[0].Name != "重命名" {
|
||||
t.Fatal("rename failed")
|
||||
}
|
||||
r.Name = "新分页"
|
||||
apply("add-tab")
|
||||
text, _ = c.archive.Text(r.FileIndex)
|
||||
doc := parseShop(text)
|
||||
if len(doc.Tabs) != 3 || len(doc.Tabs[2].Groups) != 1 {
|
||||
t.Fatal("new tab layout")
|
||||
}
|
||||
r.TabIndex = 2
|
||||
r.ItemID = "4"
|
||||
apply("add-item")
|
||||
text, _ = c.archive.Text(r.FileIndex)
|
||||
doc = parseShop(text)
|
||||
if len(doc.Tabs[2].Groups[0].Items) != 1 {
|
||||
t.Fatal("new product")
|
||||
}
|
||||
r.SourceStart = doc.Tabs[2].Groups[0].Items[0].SourceStart
|
||||
apply("delete-item")
|
||||
text, _ = c.archive.Text(r.FileIndex)
|
||||
if len(parseShop(text).Tabs[2].Groups[0].Items) != 0 {
|
||||
t.Fatal("delete entry")
|
||||
}
|
||||
apply("delete-tab")
|
||||
text, _ = c.archive.Text(r.FileIndex)
|
||||
if len(parseShop(text).Tabs) != 2 || !strings.Contains(text, "保留的消息") {
|
||||
t.Fatal("delete tab changed other sections")
|
||||
}
|
||||
if _, ok := c.archive.Find("equipment/four.equ"); !ok {
|
||||
t.Fatal("deleted item file")
|
||||
}
|
||||
}
|
||||
func TestShopEditRejectsStaleAndInvalid(t *testing.T) {
|
||||
c, s, r := editFixture(t, editableShop)
|
||||
r.Action = "rename-tab"
|
||||
r.Name = "new"
|
||||
r.Revision++
|
||||
if _, err := s.ApplyShopEdit(r); err == nil {
|
||||
t.Fatal("stale accepted")
|
||||
}
|
||||
r.Revision = c.batchRevision
|
||||
r.Name = "bad`name"
|
||||
if _, err := s.ApplyShopEdit(r); err == nil {
|
||||
t.Fatal("invalid name accepted")
|
||||
}
|
||||
r.Name = "valid"
|
||||
r.Path = "itemshop/other.shp"
|
||||
if _, err := s.ApplyShopEdit(r); err == nil {
|
||||
t.Fatal("wrong path accepted")
|
||||
}
|
||||
}
|
||||
func TestShopItemSearchScopeBeforePagination(t *testing.T) {
|
||||
for _, disk := range []bool{false, true} {
|
||||
c, _, _ := editFixture(t, editableShop)
|
||||
if _, err := c.archive.AddFileText("npc/npc2.npc", "[name]\n`材料商人`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if disk {
|
||||
index, _, err := openSQLiteArchiveIndex(c.archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.diskIndex = index
|
||||
c.installDiskArchiveIndexesLocked(c.archive)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
exact, err := NewArchiveService(c).SearchItems("2", 0, 10)
|
||||
if err != nil || len(exact.Hits) == 0 || exact.Hits[0].ID != "2" || exact.Hits[0].Name != "材料甲" {
|
||||
t.Fatalf("exact match first: %#v, %v", exact, err)
|
||||
}
|
||||
cursor := 0
|
||||
ids := []string{}
|
||||
for page := 0; page < 10; page++ {
|
||||
res, err := NewArchiveService(c).SearchItems("材料", cursor, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, hit := range res.Hits {
|
||||
if hit.Category != SearchCategoryStackable {
|
||||
t.Fatal("non-item result")
|
||||
}
|
||||
ids = append(ids, hit.ID)
|
||||
}
|
||||
if res.NextCursor < 0 {
|
||||
break
|
||||
}
|
||||
cursor = res.NextCursor
|
||||
}
|
||||
if len(ids) != 2 {
|
||||
t.Fatalf("disk=%v ids=%v", disk, ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopCategorizedTabCanBeRecreatedAfterLastDeletion(t *testing.T) {
|
||||
text := "[sell info]\n[use category]\n`expert job`\n[tab]\n`last`\n[category entry]\n[id]\n1\n[item list]\n1\n[/item list]\n[/category entry]\n[/tab]\n[/sell info]"
|
||||
c, s, r := editFixture(t, text)
|
||||
r.Action = "delete-tab"
|
||||
if _, err := s.ApplyShopEdit(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Text, _ = c.archive.Text(r.FileIndex)
|
||||
r.Revision = c.batchRevision
|
||||
r.Action = "add-tab"
|
||||
r.Name = "重新创建"
|
||||
if _, err := s.ApplyShopEdit(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, _ := c.archive.Text(r.FileIndex)
|
||||
doc := parseShop(saved)
|
||||
if len(doc.Tabs) != 1 || len(doc.Tabs[0].Groups) != 4 {
|
||||
t.Fatalf("category groups=%#v", doc.Tabs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopReadItemUsesDraftCosts(t *testing.T) {
|
||||
c, s, _ := editFixture(t, editableShop)
|
||||
i, _ := c.archive.Find("stackable/one.stk")
|
||||
item, err := s.ReadItem("1", []ShopDraft{{FileIndex: i, Path: c.archive.Path(i), Text: "[price]\n42\n[need material]\n3 6\n[/need material]"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if item.Costs[0].Quantity != "42" || item.Costs[1].ItemID != "3" {
|
||||
t.Fatalf("draft costs=%#v", item.Costs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopSearchExactIDPagination(t *testing.T) {
|
||||
for _, disk := range []bool{false, true} {
|
||||
c, _, _ := editFixture(t, editableShop)
|
||||
if _, err := c.archive.AddFileText("stackable/12.stk", "[name]\n`other`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, _ := c.archive.Find("stackable/stackable.lst")
|
||||
text, _ := c.archive.Text(list)
|
||||
if err := c.archive.SetText(list, text+"\n12 `12.stk`"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.setArchive(c.archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if disk {
|
||||
index, _, err := openSQLiteArchiveIndex(c.archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.diskIndex = index
|
||||
c.installDiskArchiveIndexesLocked(c.archive)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
ids := []string{}
|
||||
cursor := 0
|
||||
for page := 0; page < 10; page++ {
|
||||
result, err := NewArchiveService(c).SearchItems("2", cursor, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Hits) > 1 {
|
||||
t.Fatal("limit exceeded")
|
||||
}
|
||||
for _, hit := range result.Hits {
|
||||
ids = append(ids, hit.ID)
|
||||
}
|
||||
if result.NextCursor < 0 {
|
||||
break
|
||||
}
|
||||
if result.NextCursor <= cursor {
|
||||
t.Fatal("cursor did not advance")
|
||||
}
|
||||
cursor = result.NextCursor
|
||||
}
|
||||
if len(ids) != 2 || ids[0] != "2" || ids[1] != "12" {
|
||||
t.Fatalf("disk=%v ids=%v", disk, ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user