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,770 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
pvfversion "pvfine/internal/version"
|
||||
)
|
||||
|
||||
type ShopMaterialInput struct {
|
||||
ItemID string `json:"itemId"`
|
||||
Quantity string `json:"quantity"`
|
||||
}
|
||||
type ShopDraft struct {
|
||||
FileIndex int32 `json:"fileIndex"`
|
||||
Path string `json:"path"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type ShopEditRequest struct {
|
||||
FileIndex int32 `json:"fileIndex"`
|
||||
Path string `json:"path"`
|
||||
Text string `json:"text"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Action string `json:"action"`
|
||||
TabIndex int `json:"tabIndex"`
|
||||
SourceStart int `json:"sourceStart"`
|
||||
CategoryID string `json:"categoryId"`
|
||||
ItemID string `json:"itemId"`
|
||||
Name string `json:"name"`
|
||||
SetGold bool `json:"setGold"`
|
||||
Gold string `json:"gold"` // empty removes [price], "0" is an explicit zero
|
||||
SetMaterials bool `json:"setMaterials"`
|
||||
Materials []ShopMaterialInput `json:"materials"`
|
||||
Drafts []ShopDraft `json:"drafts"`
|
||||
}
|
||||
type ShopEditedFile struct {
|
||||
FileIndex int32 `json:"fileIndex"`
|
||||
Path string `json:"path"`
|
||||
BeforeText string `json:"beforeText"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type ShopEditResult struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
Files []ShopEditedFile `json:"files"`
|
||||
AffectedItems int `json:"affectedItems"`
|
||||
ModifiedCount int `json:"modifiedCount"`
|
||||
}
|
||||
|
||||
// ReadItem resolves the same item union used by shop lists and search pickers.
|
||||
func (s *FileGUIService) ReadItem(id string, drafts []ShopDraft) (*ShopItem, error) {
|
||||
s.c.mu.Lock()
|
||||
defer s.c.mu.Unlock()
|
||||
if s.c.archive == nil {
|
||||
return nil, ErrNoArchive
|
||||
}
|
||||
if s.c.annotationEngine == nil {
|
||||
return nil, fmt.Errorf("物品索引未初始化")
|
||||
}
|
||||
ref, ok := s.c.resolveAnnotationReferenceLocked("物品", id)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("找不到物品 ID %s", id)
|
||||
}
|
||||
for _, draft := range drafts {
|
||||
if draft.FileIndex == ref.FileIndex {
|
||||
if !sameSearchPath(draft.Path, s.c.archive.Path(ref.FileIndex)) {
|
||||
return nil, fmt.Errorf("物品草稿已变化")
|
||||
}
|
||||
return s.readItemLocked(id, &draft.Text)
|
||||
}
|
||||
}
|
||||
return s.readItemLocked(id, nil)
|
||||
}
|
||||
func (s *FileGUIService) readItemLocked(id string, draftText *string) (*ShopItem, error) {
|
||||
if s.c.annotationEngine == nil {
|
||||
return nil, fmt.Errorf("物品索引未初始化")
|
||||
}
|
||||
ref, ok := s.c.resolveAnnotationReferenceLocked("物品", id)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("找不到物品 ID %s", id)
|
||||
}
|
||||
a := s.c.archive
|
||||
meta, err := a.ScriptMetadata(ref.FileIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text, err := a.Text(ref.FileIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if draftText != nil {
|
||||
text = *draftText
|
||||
}
|
||||
doc := &ShopDocument{}
|
||||
item := &ShopItem{ID: id, FileIndex: ref.FileIndex, Path: a.Path(ref.FileIndex), Name: markedName(meta), Rarity: meta.RarityValue(), Icon: imageReferenceFromPVF(meta.Icon), Costs: shopCosts(text, id, doc)}
|
||||
if len(doc.Issues) > 0 {
|
||||
return nil, fmt.Errorf("物品成本格式异常,请先在文本模式修正:%s", doc.Issues[0].Message)
|
||||
}
|
||||
for i := range item.Costs {
|
||||
cost := &item.Costs[i]
|
||||
if cost.Kind != "material" {
|
||||
continue
|
||||
}
|
||||
if m, ok := s.c.resolveAnnotationReferenceLocked("物品", cost.ItemID); ok {
|
||||
cost.Name = m.Name
|
||||
if metadata, err := a.ScriptMetadata(m.FileIndex); err == nil {
|
||||
cost.Name = markedName(metadata)
|
||||
cost.Icon = imageReferenceFromPVF(metadata.Icon)
|
||||
}
|
||||
} else {
|
||||
cost.Name = "未知物品 #" + cost.ItemID
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// ApplyShopEdit stages every touched file before publishing one overlay change.
|
||||
// The caller sends dirty drafts, but only files touched by this operation are
|
||||
// consumed. Revision and path checks reject another archive or stale dialog.
|
||||
func (s *FileGUIService) ApplyShopEdit(req ShopEditRequest) (*ShopEditResult, error) {
|
||||
s.c.mu.Lock()
|
||||
result, a, summary, err := s.applyShopEditLocked(req)
|
||||
s.c.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Files) > 0 {
|
||||
if err := s.c.scheduleArchiveMutations(a, summary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexes := make([]int32, 0, len(result.Files))
|
||||
for _, f := range result.Files {
|
||||
indexes = append(indexes, f.FileIndex)
|
||||
}
|
||||
emitEvent("archive:batch-applied", map[string]any{"source": "shop-gui", "structural": false, "fileIndexes": indexes, "revision": result.Revision, "modifiedCount": result.ModifiedCount})
|
||||
emitEvent("archive:advanced-search-stale", map[string]any{"shop": true})
|
||||
emitVersionState(s.c, "shop-edited")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *FileGUIService) applyShopEditLocked(req ShopEditRequest) (*ShopEditResult, *pvf.Archive, pvf.MutationSummary, error) {
|
||||
fail := func(err error) (*ShopEditResult, *pvf.Archive, pvf.MutationSummary, error) {
|
||||
return nil, nil, pvf.MutationSummary{}, err
|
||||
}
|
||||
a := s.c.archive
|
||||
if a == nil {
|
||||
return fail(ErrNoArchive)
|
||||
}
|
||||
if req.Revision != s.c.batchRevision {
|
||||
return fail(fmt.Errorf("商店数据已变化,请关闭表单并重新加载后再操作"))
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := validateAnnotationIndex(a, req.FileIndex); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if !sameSearchPath(req.Path, a.Path(req.FileIndex)) || !strings.HasSuffix(strings.ToLower(req.Path), ".shp") {
|
||||
return fail(fmt.Errorf("商店文件已变化"))
|
||||
}
|
||||
limit := int(s.c.editableByteLimit())
|
||||
if len(req.Text) > limit {
|
||||
return fail(fmt.Errorf("商店文本过大"))
|
||||
}
|
||||
parsed := parseShop(req.Text)
|
||||
if len(parsed.Issues) > 0 && !(req.Action == "add-tab" && len(parsed.Tabs) == 0 && len(parsed.Issues) == 1) {
|
||||
return fail(fmt.Errorf("商店结构异常,请先在文本模式修正:%s", parsed.Issues[0].Message))
|
||||
}
|
||||
if parsed.CategoryType != "" {
|
||||
categories, categoryErr := readShopCategories(a, parsed.CategoryType)
|
||||
if categoryErr != nil {
|
||||
return fail(categoryErr)
|
||||
}
|
||||
parsed.Categories = categories
|
||||
}
|
||||
stage := a.CloneForBatch()
|
||||
drafts := make(map[int32]ShopDraft)
|
||||
for _, d := range req.Drafts {
|
||||
if d.FileIndex < 0 || d.FileIndex >= a.FileCount() || !sameSearchPath(d.Path, a.Path(d.FileIndex)) {
|
||||
return fail(fmt.Errorf("草稿文件已变化"))
|
||||
}
|
||||
if _, ok := drafts[d.FileIndex]; ok {
|
||||
return fail(fmt.Errorf("重复的文件草稿"))
|
||||
}
|
||||
drafts[d.FileIndex] = d
|
||||
}
|
||||
drafts[req.FileIndex] = ShopDraft{FileIndex: req.FileIndex, Path: req.Path, Text: req.Text}
|
||||
loaded := make(map[int32]bool)
|
||||
beforeTexts := make(map[int32]string)
|
||||
load := func(index int32) (*pvf.ScriptDocument, error) {
|
||||
if !loaded[index] {
|
||||
text, err := a.Text(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cached, ok := s.c.editorText[index]; ok {
|
||||
text = cached
|
||||
}
|
||||
if draft, ok := drafts[index]; ok {
|
||||
text = draft.Text
|
||||
}
|
||||
if len(text) > limit {
|
||||
return nil, fmt.Errorf("待修改文件超过编辑大小限制")
|
||||
}
|
||||
beforeTexts[index] = text
|
||||
rawText, err := a.Text(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if text != rawText {
|
||||
if err := stage.SetText(index, text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
loaded[index] = true
|
||||
}
|
||||
raw, err := stage.RawBytes(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stage.ParseScriptDocument(raw)
|
||||
}
|
||||
document, err := load(req.FileIndex)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if len(document.Warnings()) > 0 {
|
||||
return fail(fmt.Errorf("商店区块未正确闭合,请先在文本模式修正"))
|
||||
}
|
||||
sell := document.Sections([]string{"sell info"})
|
||||
var tab *pvf.ScriptSection
|
||||
if req.Action != "add-tab" {
|
||||
if req.TabIndex < 0 || req.TabIndex >= len(parsed.Tabs) {
|
||||
return fail(fmt.Errorf("分页已变化,请重新加载"))
|
||||
}
|
||||
location := parsed.Tabs[req.TabIndex].location
|
||||
matches := document.Sections(location.path)
|
||||
if location.occurrence >= len(matches) {
|
||||
return fail(fmt.Errorf("分页结构已变化"))
|
||||
}
|
||||
tab = matches[location.occurrence]
|
||||
if location.implicit && (req.Action == "rename-tab" || req.Action == "delete-tab") {
|
||||
return fail(fmt.Errorf("该商店没有显式分页,不能重命名或删除分页;可直接编辑商品"))
|
||||
}
|
||||
}
|
||||
if req.SetGold || req.SetMaterials {
|
||||
for ti, page := range parsed.Tabs {
|
||||
if ti != req.TabIndex {
|
||||
continue
|
||||
}
|
||||
for _, group := range page.Groups {
|
||||
for _, entry := range group.Items {
|
||||
if req.Action == "batch-costs" || req.Action == "edit-item" && entry.SourceStart == req.SourceStart {
|
||||
if entry.localGold || entry.localMaterials {
|
||||
return fail(fmt.Errorf("该商品有商店局部价格或兑换材料,请在文本模式修改局部成本;未修改共享物品成本"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
itemIDs := []string{}
|
||||
shopChanged := false
|
||||
switch req.Action {
|
||||
case "add-tab", "rename-tab":
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" || strings.ContainsAny(name, "`\r\n") || len([]rune(name)) > 100 {
|
||||
return fail(fmt.Errorf("分页名称需为 1–100 个字符,不能包含换行或反引号"))
|
||||
}
|
||||
value, err := pvf.NewScriptValue(pvf.ScriptTokenQuoted, name, pvf.ScriptPoolUTF16)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if req.Action == "rename-tab" {
|
||||
err = tab.SetValues([]pvf.ScriptValue{value})
|
||||
} else {
|
||||
for _, page := range parsed.Tabs {
|
||||
if page.location.implicit {
|
||||
return fail(fmt.Errorf("该商店没有显式分页,新增分页需先在文本模式调整结构"))
|
||||
}
|
||||
}
|
||||
var lists []*pvf.ScriptSection
|
||||
for _, page := range parsed.Tabs {
|
||||
matches := document.Sections(page.location.path)
|
||||
if page.location.occurrence >= len(matches) {
|
||||
return fail(fmt.Errorf("分页结构已变化"))
|
||||
}
|
||||
lists = append(lists, shopItemLists(matches[page.location.occurrence])...)
|
||||
}
|
||||
listName := shopListName(lists, a.ClientVersion())
|
||||
if len(parsed.Tabs) > 0 && len(parsed.Tabs[0].location.path) == 1 {
|
||||
tab, err = document.AppendSection("tab", []pvf.ScriptValue{value}, true)
|
||||
} else if len(sell) == 1 && sell[0].HasEndTag() {
|
||||
tab, err = sell[0].AppendSection("tab", []pvf.ScriptValue{value}, true)
|
||||
} else {
|
||||
return fail(fmt.Errorf("无法确定新增分页所属的完整商店区块"))
|
||||
}
|
||||
if err == nil {
|
||||
if parsed.CategoryType == "" {
|
||||
_, err = tab.AppendSection(listName, nil, true)
|
||||
} else {
|
||||
if len(parsed.Categories) == 0 {
|
||||
return fail(fmt.Errorf("商店没有可用分类,请检查商店分类文件"))
|
||||
}
|
||||
for _, category := range parsed.Categories {
|
||||
group, e := tab.AppendSection("category entry", nil, true)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
id, e := shopInteger(category.ID)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
if _, e = group.AppendSection("id", []pvf.ScriptValue{id}, false); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
if _, e = group.AppendSection(listName, nil, true); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
shopChanged = true
|
||||
case "delete-tab":
|
||||
if err := tab.Delete(); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
shopChanged = true
|
||||
case "edit-item", "delete-item":
|
||||
ordinal := -1
|
||||
count := 0
|
||||
for _, group := range parsed.Tabs[req.TabIndex].Groups {
|
||||
for _, entry := range group.Items {
|
||||
if entry.SourceStart == req.SourceStart {
|
||||
ordinal = count
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
if ordinal < 0 {
|
||||
return fail(fmt.Errorf("商品位置已变化,请重新打开表单"))
|
||||
}
|
||||
records := shopRecords(tab)
|
||||
if ordinal >= len(records) {
|
||||
return fail(fmt.Errorf("无法定位商品记录"))
|
||||
}
|
||||
record := records[ordinal]
|
||||
target, offset := record.index, record.offset
|
||||
values := target.Values()
|
||||
if req.Action == "delete-item" {
|
||||
if record.block != nil {
|
||||
if err := record.block.Delete(); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
} else {
|
||||
values = append(values[:offset], values[offset+1:]...)
|
||||
}
|
||||
} else {
|
||||
value, err := shopInteger(req.ItemID)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if _, err = s.readItemLocked(req.ItemID, nil); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
values[offset] = value
|
||||
itemIDs = append(itemIDs, req.ItemID)
|
||||
}
|
||||
if req.Action != "delete-item" || record.block == nil {
|
||||
if err := target.SetValues(values); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
shopChanged = true
|
||||
case "add-item":
|
||||
value, err := shopInteger(req.ItemID)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if _, err = s.readItemLocked(req.ItemID, nil); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
var target *pvf.ScriptSection
|
||||
listName := shopListName(shopItemLists(tab), a.ClientVersion())
|
||||
if parsed.CategoryType == "" {
|
||||
for _, child := range tab.Children() {
|
||||
if child.Name() == "item list" || child.Name() == "sell item list" {
|
||||
target = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
target, err = tab.AppendSection(listName, nil, true)
|
||||
}
|
||||
} else {
|
||||
for _, group := range tab.Children() {
|
||||
if group.Name() != "category entry" {
|
||||
continue
|
||||
}
|
||||
id := ""
|
||||
for _, child := range group.Children() {
|
||||
if child.Name() == "id" {
|
||||
if v, ok := child.Get(0); ok {
|
||||
id = fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if id != req.CategoryID {
|
||||
continue
|
||||
}
|
||||
for _, child := range group.Children() {
|
||||
if child.Name() == "item list" || child.Name() == "sell item list" {
|
||||
target = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
target, err = group.AppendSection(listName, nil, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if target == nil {
|
||||
known := false
|
||||
for _, category := range parsed.Categories {
|
||||
if category.ID == req.CategoryID {
|
||||
known = true
|
||||
}
|
||||
}
|
||||
if !known {
|
||||
return fail(fmt.Errorf("当前商店没有该分类,无法添加商品"))
|
||||
}
|
||||
group, e := tab.AppendSection("category entry", nil, true)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
id, e := shopInteger(req.CategoryID)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
if _, e = group.AppendSection("id", []pvf.ScriptValue{id}, false); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
target, e = group.AppendSection(listName, nil, true)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
}
|
||||
if err := appendShopRecord(document, target, value); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
itemIDs = append(itemIDs, req.ItemID)
|
||||
shopChanged = true
|
||||
case "batch-costs":
|
||||
if !req.SetGold && !req.SetMaterials {
|
||||
return fail(fmt.Errorf("请选择至少一种需要批量设置的成本"))
|
||||
}
|
||||
for _, record := range shopRecords(tab) {
|
||||
itemIDs = append(itemIDs, fmt.Sprint(record.index.Values()[record.offset].Value))
|
||||
}
|
||||
if len(itemIDs) == 0 {
|
||||
return fail(fmt.Errorf("当前分页没有商品"))
|
||||
}
|
||||
default:
|
||||
return fail(fmt.Errorf("未知商店操作"))
|
||||
}
|
||||
if shopChanged {
|
||||
raw, err := stage.EncodeScriptDocument(document)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := stage.SetRawBytes(req.FileIndex, raw); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
var gold []pvf.ScriptValue
|
||||
if req.SetGold && req.Gold != "" {
|
||||
v, err := shopInteger(req.Gold)
|
||||
if err != nil {
|
||||
return fail(fmt.Errorf("金币价格无效:%w", err))
|
||||
}
|
||||
gold = []pvf.ScriptValue{v}
|
||||
}
|
||||
materials := []pvf.ScriptValue{}
|
||||
if req.SetMaterials {
|
||||
for _, m := range req.Materials {
|
||||
id, err := shopInteger(m.ItemID)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
qty, err := shopInteger(m.Quantity)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if _, ok := s.c.resolveAnnotationReferenceLocked("物品", m.ItemID); !ok {
|
||||
return fail(fmt.Errorf("找不到兑换道具 %s", m.ItemID))
|
||||
}
|
||||
materials = append(materials, id, qty)
|
||||
}
|
||||
}
|
||||
seen := make(map[int32]bool)
|
||||
for _, id := range itemIDs {
|
||||
ref, ok := s.c.resolveAnnotationReferenceLocked("物品", id)
|
||||
if !ok {
|
||||
return fail(fmt.Errorf("找不到商品 %s,未应用任何修改", id))
|
||||
}
|
||||
if seen[ref.FileIndex] {
|
||||
continue
|
||||
}
|
||||
seen[ref.FileIndex] = true
|
||||
if !req.SetGold && !req.SetMaterials {
|
||||
continue
|
||||
}
|
||||
itemDocument, err := load(ref.FileIndex)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if req.SetGold {
|
||||
if err := replaceShopCostSection(itemDocument, "price", gold, false); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
if req.SetMaterials {
|
||||
if err := replaceShopCostSection(itemDocument, "need material", materials, true); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
raw, err := stage.EncodeScriptDocument(itemDocument)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := stage.SetRawBytes(ref.FileIndex, raw); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
indexes := make([]int32, 0, len(loaded))
|
||||
for i := range loaded {
|
||||
indexes = append(indexes, i)
|
||||
}
|
||||
sort.Slice(indexes, func(i, j int) bool { return indexes[i] < indexes[j] })
|
||||
changes := []pvf.ScriptChange{}
|
||||
paths := []string{}
|
||||
result := &ShopEditResult{Files: []ShopEditedFile{}, AffectedItems: len(seen)}
|
||||
for _, i := range indexes {
|
||||
before, err := a.RawBytes(i)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
after, err := stage.RawBytes(i)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if bytes.Equal(before, after) {
|
||||
continue
|
||||
}
|
||||
text, err := stage.Text(i)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
changes = append(changes, pvf.ScriptChange{Kind: pvf.ChangeKindChanged, Path: a.Path(i), Raw: after, DataType: a.File(i).DataType})
|
||||
paths = append(paths, a.Path(i))
|
||||
result.Files = append(result.Files, ShopEditedFile{FileIndex: i, Path: a.Path(i), BeforeText: beforeTexts[i], Text: text})
|
||||
}
|
||||
var before, after pvfversion.ContentSnapshot
|
||||
if len(changes) > 0 && s.c.versionRepo != nil {
|
||||
before, err = pvfversion.ContentSnapshotFromArchive(a, paths)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
after, err = pvfversion.ContentSnapshotFromArchive(stage, paths)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
checkpoint := a.MutationCheckpoint()
|
||||
if len(changes) > 0 {
|
||||
changedIndexes := make(map[int32]struct{}, len(result.Files))
|
||||
for _, file := range result.Files {
|
||||
changedIndexes[file.FileIndex] = struct{}{}
|
||||
}
|
||||
if s.c.diskIndex != nil {
|
||||
if err := s.c.diskIndex.refreshFileMetadata(stage, changedIndexes); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
if err := a.ApplyScriptChanges(stage, changes); err != nil {
|
||||
if s.c.diskIndex != nil {
|
||||
_ = s.c.diskIndex.refreshFileMetadata(a, changedIndexes)
|
||||
}
|
||||
return fail(err)
|
||||
}
|
||||
if s.c.diskIndex == nil {
|
||||
refreshArchiveIndexMetadataLocked(s.c, changedIndexes)
|
||||
}
|
||||
if err := s.c.recordVersionMutationLocked("商店编辑", before, after); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
s.c.batchRevision++
|
||||
s.c.batchPlan = nil
|
||||
s.c.invalidateScriptLocked()
|
||||
s.c.markAdvancedSearchStaleLocked(s.c.archive)
|
||||
s.c.editorAnnotation = editorAnnotationCache{}
|
||||
s.c.annotationRelations = make(map[string]map[string]*relationTarget)
|
||||
if s.c.editorText == nil {
|
||||
s.c.editorText = make(map[int32]string)
|
||||
}
|
||||
for _, f := range result.Files {
|
||||
s.c.editorText[f.FileIndex] = f.Text
|
||||
delete(s.c.visualsByFile, f.FileIndex)
|
||||
}
|
||||
}
|
||||
summary := a.MutationsSince(checkpoint)
|
||||
a.ClearMutations()
|
||||
result.Revision = s.c.batchRevision
|
||||
result.ModifiedCount = a.ModifiedCount()
|
||||
return result, a, summary, nil
|
||||
}
|
||||
|
||||
func shopInteger(text string) (pvf.ScriptValue, error) {
|
||||
n, err := strconv.ParseInt(text, 10, 32)
|
||||
if err != nil || n < 0 {
|
||||
return pvf.ScriptValue{}, fmt.Errorf("需要 0–2147483647 的整数:%s", text)
|
||||
}
|
||||
return pvf.NewScriptValue(pvf.ScriptTokenInteger, n, "")
|
||||
}
|
||||
func shopItemLists(tab *pvf.ScriptSection) []*pvf.ScriptSection {
|
||||
lists := []*pvf.ScriptSection{}
|
||||
for _, child := range tab.Children() {
|
||||
switch child.Name() {
|
||||
case "item list", "sell item list":
|
||||
lists = append(lists, child)
|
||||
case "category entry":
|
||||
for _, list := range child.Children() {
|
||||
if list.Name() == "item list" || list.Name() == "sell item list" {
|
||||
lists = append(lists, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lists
|
||||
}
|
||||
|
||||
func shopListName(lists []*pvf.ScriptSection, version string) string {
|
||||
for _, list := range lists {
|
||||
if list.Name() == "sell item list" {
|
||||
return "sell item list"
|
||||
}
|
||||
}
|
||||
if len(lists) == 0 && version == "115CN" {
|
||||
return "sell item list"
|
||||
}
|
||||
return "item list"
|
||||
}
|
||||
|
||||
type shopRecord struct {
|
||||
index *pvf.ScriptSection
|
||||
offset int
|
||||
block *pvf.ScriptSection
|
||||
}
|
||||
|
||||
func shopRecords(tab *pvf.ScriptSection) []shopRecord {
|
||||
records := []shopRecord{}
|
||||
for _, list := range shopItemLists(tab) {
|
||||
if list.Name() == "item list" {
|
||||
for i := range list.Values() {
|
||||
records = append(records, shopRecord{index: list, offset: i})
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, block := range list.Children() {
|
||||
if block.Name() != "item" {
|
||||
continue
|
||||
}
|
||||
for _, field := range block.Children() {
|
||||
if field.Name() == "index" {
|
||||
values := field.Values()
|
||||
if len(values) == 1 && values[0].Type == pvf.ScriptTokenInteger && fmt.Sprint(values[0].Value) == "-1" {
|
||||
continue
|
||||
}
|
||||
records = append(records, shopRecord{index: field, block: block})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func appendShopRecord(document *pvf.ScriptDocument, list *pvf.ScriptSection, value pvf.ScriptValue) error {
|
||||
if list.Name() == "item list" {
|
||||
return list.Append(value)
|
||||
}
|
||||
// The direct item integer is the shop entry key, not the product ID.
|
||||
// Allocate above existing keys without renumbering records or copying overrides.
|
||||
var next int64
|
||||
var blocks []*pvf.ScriptSection
|
||||
var collect func([]*pvf.ScriptSection)
|
||||
collect = func(sections []*pvf.ScriptSection) {
|
||||
for _, section := range sections {
|
||||
if section.Name() == "sell item list" {
|
||||
for _, block := range section.Children() {
|
||||
if block.Name() == "item" {
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collect(section.Children())
|
||||
}
|
||||
}
|
||||
}
|
||||
collect(document.Sections(nil))
|
||||
for _, block := range blocks {
|
||||
values := block.Values()
|
||||
if len(values) != 1 {
|
||||
return fmt.Errorf("商品记录序号结构异常,无法新增")
|
||||
}
|
||||
key, err := strconv.ParseInt(fmt.Sprint(values[0].Value), 10, 32)
|
||||
if err != nil || key < 0 || values[0].Type != pvf.ScriptTokenInteger {
|
||||
return fmt.Errorf("商品记录序号无效")
|
||||
}
|
||||
if key >= next {
|
||||
next = key + 1
|
||||
}
|
||||
}
|
||||
key, err := shopInteger(strconv.FormatInt(next, 10))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := list.AppendSection("item", []pvf.ScriptValue{key}, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = block.AppendSection("index", []pvf.ScriptValue{value}, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func replaceShopCostSection(doc *pvf.ScriptDocument, name string, values []pvf.ScriptValue, paired bool) error {
|
||||
sections := doc.Sections([]string{name})
|
||||
if len(values) == 0 {
|
||||
for _, section := range sections {
|
||||
if err := section.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(sections) == 0 {
|
||||
_, err := doc.AppendSection(name, values, paired)
|
||||
return err
|
||||
}
|
||||
if err := sections[0].SetValues(values); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, section := range sections[1:] {
|
||||
if err := section.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user