DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+770
View File
@@ -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
}