服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
215 lines
5.9 KiB
Go
215 lines
5.9 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"pvfine/internal/pvf"
|
|
)
|
|
|
|
type shopLocation struct {
|
|
path []string
|
|
occurrence int
|
|
implicit bool
|
|
}
|
|
|
|
type shopNode struct {
|
|
element pvf.ScriptElement
|
|
values []pvf.ScriptElement
|
|
children []*shopNode
|
|
location shopLocation
|
|
}
|
|
|
|
func parseShop(text string) *ShopDocument {
|
|
doc := &ShopDocument{Categories: []ShopCategory{}, Tabs: []ShopTab{}, Issues: []PreviewIssue{}}
|
|
root := &shopNode{}
|
|
stack := []*shopNode{root}
|
|
nodes := map[int]*shopNode{}
|
|
ordered := []*shopNode{}
|
|
counts := map[string]int{}
|
|
for _, e := range pvf.ParseScriptView(text).Elements {
|
|
if e.Kind == pvf.ScriptElementToken {
|
|
if n := nodes[e.SectionID]; n != nil {
|
|
n.values = append(n.values, e)
|
|
}
|
|
continue
|
|
}
|
|
if e.Kind != pvf.ScriptElementSection {
|
|
continue
|
|
}
|
|
depth := len(e.SectionPath)
|
|
if depth > len(stack) {
|
|
continue
|
|
}
|
|
stack = stack[:depth]
|
|
key := strings.Join(e.SectionPath, "/")
|
|
n := &shopNode{element: e, location: shopLocation{path: e.SectionPath, occurrence: counts[key]}}
|
|
counts[key]++
|
|
parent := stack[len(stack)-1]
|
|
parent.children = append(parent.children, n)
|
|
nodes[e.SectionID] = n
|
|
ordered = append(ordered, n)
|
|
stack = append(stack, n)
|
|
}
|
|
categoryTypes := 0
|
|
for _, n := range ordered {
|
|
if strings.Join(n.element.SectionPath, "/") == "sell info/use category" {
|
|
categoryTypes++
|
|
doc.CategoryType = shopNodeFirst(n)
|
|
}
|
|
}
|
|
if categoryTypes > 1 {
|
|
shopIssue(doc, "use category", "商店包含重复的大分类声明")
|
|
}
|
|
consumed := map[*shopNode]bool{}
|
|
addTab := func(n *shopNode, implicit bool) {
|
|
name := shopNodeFirst(n)
|
|
if implicit {
|
|
name = "商品"
|
|
}
|
|
if name == "" {
|
|
name = fmt.Sprintf("分页 %d", len(doc.Tabs)+1)
|
|
}
|
|
tab := ShopTab{Name: name, SourceStart: n.element.Start, Groups: []ShopGroup{}, location: n.location}
|
|
tab.location.implicit = implicit
|
|
tab.Implicit = implicit
|
|
for _, child := range n.children {
|
|
switch child.element.Section {
|
|
case "item list", "sell item list":
|
|
group := ShopGroup{SourceStart: child.element.Start, Items: []ShopEntry{}}
|
|
parseShopList(child, &group, doc)
|
|
consumed[child] = true
|
|
tab.Groups = append(tab.Groups, group)
|
|
case "category entry":
|
|
group := ShopGroup{SourceStart: child.element.Start, Items: []ShopEntry{}}
|
|
for _, field := range child.children {
|
|
switch field.element.Section {
|
|
case "id":
|
|
group.CategoryID = shopNodeFirst(field)
|
|
case "item list", "sell item list":
|
|
parseShopList(field, &group, doc)
|
|
consumed[field] = true
|
|
}
|
|
}
|
|
if _, err := strconv.ParseInt(group.CategoryID, 10, 32); err != nil {
|
|
shopIssue(doc, "category entry", "分类 ID 无效:"+group.CategoryID)
|
|
}
|
|
tab.Groups = append(tab.Groups, group)
|
|
}
|
|
}
|
|
doc.Tabs = append(doc.Tabs, tab)
|
|
}
|
|
for _, n := range root.children {
|
|
switch n.element.Section {
|
|
case "tab":
|
|
addTab(n, false)
|
|
case "sell info":
|
|
direct := false
|
|
for _, child := range n.children {
|
|
switch child.element.Section {
|
|
case "tab":
|
|
addTab(child, false)
|
|
case "item list", "sell item list", "category entry":
|
|
direct = true
|
|
}
|
|
}
|
|
if direct {
|
|
addTab(n, true)
|
|
}
|
|
}
|
|
}
|
|
for _, n := range ordered {
|
|
if (n.element.Section == "item list" || n.element.Section == "sell item list") && !consumed[n] {
|
|
shopIssue(doc, n.element.Section, "无法识别商品列表所在的商店结构")
|
|
}
|
|
}
|
|
categories := map[string]bool{}
|
|
for _, tab := range doc.Tabs {
|
|
for _, group := range tab.Groups {
|
|
if group.CategoryID != "" && !categories[group.CategoryID] {
|
|
categories[group.CategoryID] = true
|
|
doc.Categories = append(doc.Categories, ShopCategory{ID: group.CategoryID})
|
|
}
|
|
if doc.CategoryType != "" && group.CategoryID == "" {
|
|
shopIssue(doc, "category entry", "分类商品缺少分类 ID")
|
|
}
|
|
}
|
|
}
|
|
if doc.CategoryType == "" && len(doc.Categories) > 0 {
|
|
shopIssue(doc, "use category", "存在分类商品,但未声明大分类类型")
|
|
}
|
|
if len(doc.Tabs) == 0 {
|
|
shopIssue(doc, "sell info", "未找到可展示的商店分页,请检查文本结构")
|
|
}
|
|
return doc
|
|
}
|
|
|
|
func shopNodeFirst(n *shopNode) string {
|
|
if len(n.values) > 0 {
|
|
return n.values[0].Value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseShopList(n *shopNode, group *ShopGroup, doc *ShopDocument) {
|
|
appendID := func(v pvf.ScriptElement) *ShopEntry {
|
|
id, err := strconv.ParseInt(v.Value, 10, 32)
|
|
if err != nil || id < 0 || v.TokenType != 0 {
|
|
shopIssue(doc, n.element.Section, "商品 ID 无效:"+v.Value)
|
|
return nil
|
|
}
|
|
group.Items = append(group.Items, ShopEntry{Item: ShopItem{ID: v.Value}, SourceStart: v.Start, SourceEnd: v.End})
|
|
return &group.Items[len(group.Items)-1]
|
|
}
|
|
if n.element.Section == "item list" {
|
|
for _, v := range n.values {
|
|
appendID(v)
|
|
}
|
|
return
|
|
}
|
|
if len(n.values) > 0 {
|
|
shopIssue(doc, "sell item list", "商品列表包含无法识别的直接值")
|
|
}
|
|
for _, record := range n.children {
|
|
if record.element.Section != "item" {
|
|
shopIssue(doc, "sell item list", "商品列表包含无法识别的区块")
|
|
continue
|
|
}
|
|
var index *shopNode
|
|
for _, field := range record.children {
|
|
if field.element.Section == "index" {
|
|
if index != nil {
|
|
shopIssue(doc, "index", "商品包含重复的 index 区块")
|
|
}
|
|
index = field
|
|
}
|
|
}
|
|
if index == nil || len(index.values) != 1 {
|
|
shopIssue(doc, "index", "商品缺少唯一的物品 ID")
|
|
continue
|
|
}
|
|
// An index of -1 is an intentionally empty shop slot.
|
|
if index.values[0].TokenType == 0 && index.values[0].Value == "-1" {
|
|
continue
|
|
}
|
|
entry := appendID(index.values[0])
|
|
if entry == nil {
|
|
continue
|
|
}
|
|
for _, field := range record.children {
|
|
name := field.element.Section
|
|
if name != "price" && name != "need material" {
|
|
continue
|
|
}
|
|
if name == "price" {
|
|
entry.localGold = true
|
|
} else {
|
|
entry.localMaterials = true
|
|
}
|
|
entry.HasLocalCosts = true
|
|
entry.localCosts = append(entry.localCosts, shopCostValues(name, field.values, entry.Item.ID, doc)...)
|
|
}
|
|
}
|
|
}
|