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

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

876 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pvf
import (
"bytes"
"encoding/binary"
"fmt"
"maps"
"math"
"sort"
"strconv"
"strings"
)
// batchToken is the in-memory representation of one five-byte TypeScript
// token. It intentionally mirrors type1Token, but keeps the batch editor
// implementation separate from the text encoder.
type batchToken struct {
typ byte
value int32
}
type batchSection struct {
name string
open int
close int
endExclusive int
paired bool
directTokens []int
parentSection []string
}
type batchScript struct {
tokens []batchToken
sections []batchSection
topLevelTokens []int
}
type BatchTransformResult struct {
raw []byte
warnings []string
matchCount int
changed bool
}
func (r BatchTransformResult) Raw() []byte { return append([]byte(nil), r.raw...) }
func (r BatchTransformResult) Warnings() []string { return append([]string(nil), r.warnings...) }
func (r BatchTransformResult) MatchCount() int { return r.matchCount }
func (r BatchTransformResult) Changed() bool { return r.changed }
// parseBatchScript parses a TypeScript token stream while retaining the
// section spans needed by structural edits. The pairing rule intentionally
// follows decodeScript: a section name is treated as paired when a matching
// closing tag exists anywhere in the file.
func (a *Archive) parseBatchScript(raw []byte) (batchScript, error) {
tokens, err := decodeBatchTokens(raw)
if err != nil {
return batchScript{}, err
}
pairedNames := make(map[string]bool)
for _, token := range tokens {
if token.typ != 3 {
continue
}
name, closing, ok := parseSectionTag(a.ResolveString(token.value))
if ok && closing {
pairedNames[name] = true
}
}
sections := make([]batchSection, 0)
topLevelTokens := make([]int, 0)
stack := make([]int, 0)
// Unpaired sections are not pushed onto stack by the normal PVF decoder.
// Keep the current one per nesting depth so its span ends at the next
// section tag at the same semantic depth.
activeUnpaired := make(map[int]int)
closeUnpairedAt := func(index, end int) {
if sectionIndex, ok := activeUnpaired[index]; ok {
if sections[sectionIndex].endExclusive == 0 || sections[sectionIndex].endExclusive > end {
sections[sectionIndex].endExclusive = end
}
delete(activeUnpaired, index)
}
}
for index, token := range tokens {
if token.typ == 3 {
name, closing, ok := parseSectionTag(a.ResolveString(token.value))
if ok {
depth := len(stack)
closeUnpairedAt(depth, index)
if closing {
match := -1
for position := len(stack) - 1; position >= 0; position-- {
if sections[stack[position]].name == name {
match = position
break
}
}
if match < 0 {
return batchScript{}, fmt.Errorf("未找到 section [%s] 的开始标签", name)
}
for position := len(stack) - 1; position >= match; position-- {
sectionIndex := stack[position]
if position == match {
sections[sectionIndex].close = index
sections[sectionIndex].endExclusive = index + 1
} else {
return batchScript{}, fmt.Errorf("section [%s] 在 [%s] 之前未闭合", sections[sectionIndex].name, name)
}
}
stack = stack[:match]
continue
}
section := batchSection{
name: name,
open: index,
close: -1,
paired: pairedNames[name],
parentSection: currentBatchSectionPath(sections, stack),
}
sectionIndex := len(sections)
sections = append(sections, section)
if section.paired {
stack = append(stack, sectionIndex)
} else {
activeUnpaired[depth] = sectionIndex
}
continue
}
}
sectionIndex, ok := activeUnpaired[len(stack)]
if !ok && len(stack) > 0 {
sectionIndex = stack[len(stack)-1]
ok = true
}
if !ok {
topLevelTokens = append(topLevelTokens, index)
continue
}
sections[sectionIndex].directTokens = append(sections[sectionIndex].directTokens, index)
}
for depth, sectionIndex := range activeUnpaired {
_ = depth
if sections[sectionIndex].endExclusive == 0 {
sections[sectionIndex].endExclusive = len(tokens)
}
}
for _, sectionIndex := range stack {
section := sections[sectionIndex]
if section.paired && section.close < 0 {
return batchScript{}, fmt.Errorf("section [%s] 缺少结束标签", section.name)
}
}
return batchScript{tokens: tokens, sections: sections, topLevelTokens: topLevelTokens}, nil
}
func currentBatchSectionPath(sections []batchSection, stack []int) []string {
path := make([]string, 0, len(stack))
for _, index := range stack {
path = append(path, sections[index].name)
}
return path
}
func decodeBatchTokens(raw []byte) ([]batchToken, error) {
if len(raw)%5 != 0 {
return nil, fmt.Errorf("脚本 payload 长度不是 5 的倍数")
}
tokens := make([]batchToken, len(raw)/5)
for index := range tokens {
base := index * 5
typ := raw[base]
switch typ {
case 0, 2, 3, 5, 6, 7, 8, 10:
default:
return nil, fmt.Errorf("脚本包含不支持的 token 类型: %d", typ)
}
tokens[index] = batchToken{
typ: typ,
value: int32(binary.LittleEndian.Uint32(raw[base+1:])),
}
}
return tokens, nil
}
func encodeBatchTokens(tokens []batchToken) []byte {
raw := make([]byte, len(tokens)*5)
for index, token := range tokens {
base := index * 5
raw[base] = token.typ
binary.LittleEndian.PutUint32(raw[base+1:], uint32(token.value))
}
return raw
}
func (script batchScript) matchingSections(name string) []batchSection {
name = normalizeBatchSectionName(name)
result := make([]batchSection, 0)
for _, section := range script.sections {
if section.name == name {
result = append(result, section)
}
}
return result
}
func normalizeBatchSectionName(name string) string {
name = strings.TrimSpace(name)
if strings.HasPrefix(name, "[") && strings.HasSuffix(name, "]") && len(name) > 2 {
name = name[1 : len(name)-1]
}
return strings.TrimPrefix(name, "/")
}
func (a *Archive) parseBatchValue(value string, allowMany, allowSectionTags bool) ([]batchToken, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, fmt.Errorf("值不能为空")
}
raw, err := a.encodeScript(value)
if err != nil {
return nil, err
}
tokens, err := decodeBatchTokens(raw)
if err != nil {
return nil, err
}
if !allowMany && len(tokens) != 1 {
return nil, fmt.Errorf("设置值必须只包含一个 token")
}
for _, token := range tokens {
if token.typ != 3 {
continue
}
if !allowSectionTags {
if name, _, ok := parseSectionTag(a.ResolveString(token.value)); ok {
return nil, fmt.Errorf("值不能包含 section 标签 [%s]", name)
}
}
}
return tokens, nil
}
func (a *Archive) transformBatchScript(raw []byte, operations []StructuredBatchOperation) (BatchTransformResult, error) {
script, err := a.parseBatchScript(raw)
if err != nil {
return BatchTransformResult{}, err
}
result := BatchTransformResult{raw: append([]byte(nil), raw...)}
for _, operation := range operations {
current, err := a.parseBatchScript(encodeBatchTokens(script.tokens))
if err != nil {
return BatchTransformResult{}, err
}
changed, matches, warning, err := a.applyBatchOperation(&current, operation)
if err != nil {
return BatchTransformResult{}, err
}
result.matchCount += matches
if warning != "" {
result.warnings = append(result.warnings, warning)
}
if changed {
result.changed = true
}
script = current
}
result.raw = encodeBatchTokens(script.tokens)
return result, nil
}
// TransformStructuredBatch applies structural operations to raw TypeScript
// payload bytes without changing the receiver's overlay. New string values
// are resolved through the receiver's current string-pool state.
func (a *Archive) TransformStructuredBatch(raw []byte, operations []StructuredBatchOperation) (BatchTransformResult, error) {
return a.transformBatchScript(raw, operations)
}
// StructuredBatchOperation is kept in the pvf package so the binary editor
// remains testable without depending on Wails service DTOs.
type StructuredBatchOperation struct {
Kind string
Section string
TokenIndex int
Value string
CreateIfMissing bool
Operator string
Operand string
OperandEnd string
AnchorSection string
HasEndTag bool
}
func (a *Archive) applyBatchOperation(script *batchScript, operation StructuredBatchOperation) (bool, int, string, error) {
kind := strings.ToLower(strings.TrimSpace(operation.Kind))
switch kind {
case "set":
return a.applyBatchSet(script, operation)
case "number":
return a.applyBatchNumber(script, operation)
case "delete":
return applyBatchDelete(script, operation)
case "insert":
return a.applyBatchInsert(script, operation)
default:
return false, 0, "", fmt.Errorf("未知结构化操作: %s", operation.Kind)
}
}
func (a *Archive) applyBatchSet(script *batchScript, operation StructuredBatchOperation) (bool, int, string, error) {
targets := script.matchingSections(operation.Section)
if len(targets) == 0 {
if operation.CreateIfMissing {
values, err := a.parseBatchValue(operation.Value, true, true)
if err != nil {
return false, 0, "", err
}
return a.appendBatchSetSection(script, operation, values)
}
return false, 0, fmt.Sprintf("缺少 section [%s]", normalizeBatchSectionName(operation.Section)), nil
}
values, err := a.parseBatchValue(operation.Value, true, true)
if err != nil {
return false, 0, "", err
}
if operation.TokenIndex < 0 {
return a.applyBatchSetAll(script, targets, values, normalizeBatchSectionName(operation.Section))
}
type replacementTarget struct {
index int
}
replacements := make([]replacementTarget, 0, len(targets))
for _, section := range targets {
if operation.TokenIndex < 0 || operation.TokenIndex >= len(section.directTokens) {
continue
}
replacements = append(replacements, replacementTarget{
index: section.directTokens[operation.TokenIndex],
})
}
if len(replacements) == 0 {
return false, 0, fmt.Sprintf("section [%s] 没有第 %d 个直接值", normalizeBatchSectionName(operation.Section), operation.TokenIndex), nil
}
sort.Slice(replacements, func(left, right int) bool {
return replacements[left].index > replacements[right].index
})
changed := false
for _, replacement := range replacements {
if len(values) != 1 || script.tokens[replacement.index] != values[0] {
changed = true
}
updated := make([]batchToken, 0, len(script.tokens)-1+len(values))
updated = append(updated, script.tokens[:replacement.index]...)
updated = append(updated, values...)
updated = append(updated, script.tokens[replacement.index+1:]...)
script.tokens = updated
}
if _, err := a.parseBatchScript(encodeBatchTokens(script.tokens)); err != nil {
return false, 0, "", fmt.Errorf("设置值中的 section 结构无效: %w", err)
}
if len(replacements) < len(targets) {
return changed, len(replacements), fmt.Sprintf("section [%s] 有 %d 个重复项缺少第 %d 个直接值", normalizeBatchSectionName(operation.Section), len(targets)-len(replacements), operation.TokenIndex), nil
}
return changed, len(replacements), "", nil
}
func (a *Archive) applyBatchSetAll(script *batchScript, targets []batchSection, values []batchToken, sectionName string) (bool, int, string, error) {
valueScript, err := a.parseBatchScript(encodeBatchTokens(values))
if err != nil {
return false, 0, "", fmt.Errorf("设置值中的 section 结构无效: %w", err)
}
newValueCount := len(valueScript.topLevelTokens)
removeIndexes := make(map[int]struct{})
insertions := make(map[int][][]batchToken)
changed := false
countMismatches := make(map[int]int)
typeMismatches := make(map[[2]byte]int)
newValues := make([]batchToken, 0, len(valueScript.topLevelTokens))
for _, index := range valueScript.topLevelTokens {
newValues = append(newValues, values[index])
}
for _, section := range targets {
oldValues := make([]batchToken, 0, len(section.directTokens))
for _, index := range section.directTokens {
oldValues = append(oldValues, script.tokens[index])
removeIndexes[index] = struct{}{}
}
if len(oldValues) != len(values) || !batchTokensEqual(oldValues, values) {
changed = true
}
if len(oldValues) != newValueCount {
countMismatches[len(oldValues)]++
}
for index := 0; index < len(oldValues) && index < len(newValues); index++ {
if oldValues[index].typ != newValues[index].typ {
typeMismatches[[2]byte{oldValues[index].typ, newValues[index].typ}]++
}
}
insertAt := section.open + 1
if len(section.directTokens) > 0 {
insertAt = section.directTokens[0]
}
insertions[insertAt] = append(insertions[insertAt], append([]batchToken(nil), values...))
}
updated := make([]batchToken, 0, len(script.tokens)-len(removeIndexes)+len(values)*len(targets))
for index := 0; index <= len(script.tokens); index++ {
for _, insertion := range insertions[index] {
updated = append(updated, insertion...)
}
if index == len(script.tokens) {
break
}
if _, remove := removeIndexes[index]; remove {
continue
}
updated = append(updated, script.tokens[index])
}
if _, err := a.parseBatchScript(encodeBatchTokens(updated)); err != nil {
return false, 0, "", fmt.Errorf("设置值中的 section 结构无效: %w", err)
}
script.tokens = updated
warningParts := make([]string, 0, len(countMismatches))
counts := make([]int, 0, len(countMismatches))
for oldCount := range countMismatches {
counts = append(counts, oldCount)
}
sort.Ints(counts)
for _, oldCount := range counts {
warningParts = append(warningParts, fmt.Sprintf(
"section [%s] 有 %d 个匹配项的 value 数量为 %d,设置值数量为 %d",
sectionName,
countMismatches[oldCount],
oldCount,
newValueCount,
))
}
typePairs := make([][2]byte, 0, len(typeMismatches))
for pair := range typeMismatches {
typePairs = append(typePairs, pair)
}
sort.Slice(typePairs, func(left, right int) bool {
if typePairs[left][0] != typePairs[right][0] {
return typePairs[left][0] < typePairs[right][0]
}
return typePairs[left][1] < typePairs[right][1]
})
for _, pair := range typePairs {
warningParts = append(warningParts, fmt.Sprintf(
"section [%s] 有 %d 个 value 类型不一致(%s → %s)",
sectionName,
typeMismatches[pair],
batchTokenTypeName(pair[0]),
batchTokenTypeName(pair[1]),
))
}
return changed, len(targets), strings.Join(warningParts, ";"), nil
}
func batchTokensEqual(left, right []batchToken) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
func batchTokenTypeName(tokenType byte) string {
switch tokenType {
case 0:
return "整数"
case 2:
return "浮点数"
case 3:
return "字符串"
case 5:
return "块值(5)"
case 6:
return "带引号字符串"
case 7:
return "块值(7)"
case 8:
return "块值(8)"
case 10:
return "块值(10)"
default:
return fmt.Sprintf("token(%d)", tokenType)
}
}
func (a *Archive) appendBatchSetSection(script *batchScript, operation StructuredBatchOperation, values []batchToken) (bool, int, string, error) {
sectionName := normalizeBatchSectionName(operation.Section)
if sectionName == "" {
return false, 0, "", fmt.Errorf("设置 section 名称不能为空")
}
inserted := make([]batchToken, 0, len(values)+2)
inserted = append(inserted, batchToken{typ: 3, value: a.StringOffset("[" + sectionName + "]")})
inserted = append(inserted, values...)
if operation.HasEndTag {
inserted = append(inserted, batchToken{typ: 3, value: a.StringOffset("[/" + sectionName + "]")})
}
updated := make([]batchToken, 0, len(script.tokens)+len(inserted))
updated = append(updated, script.tokens...)
updated = append(updated, inserted...)
if _, err := a.parseBatchScript(encodeBatchTokens(updated)); err != nil {
return false, 0, "", fmt.Errorf("插入 section 结构无效: %w", err)
}
script.tokens = updated
return true, 1, "", nil
}
func (a *Archive) applyBatchNumber(script *batchScript, operation StructuredBatchOperation) (bool, int, string, error) {
targets := script.matchingSections(operation.Section)
if len(targets) == 0 {
return false, 0, fmt.Sprintf("缺少 section [%s]", normalizeBatchSectionName(operation.Section)), nil
}
operator := strings.TrimSpace(operation.Operator)
operand, err := strconv.ParseFloat(strings.TrimSpace(operation.Operand), 64)
if err != nil {
return false, 0, "", fmt.Errorf("数值操作数无效: %q", operation.Operand)
}
operandEnd := 0.0
if operator == "clamp" {
operandEnd, err = strconv.ParseFloat(strings.TrimSpace(operation.OperandEnd), 64)
if err != nil {
return false, 0, "", fmt.Errorf("clamp 上限无效: %q", operation.OperandEnd)
}
if operand > operandEnd {
return false, 0, "", fmt.Errorf("clamp 下限不能大于上限")
}
}
if operator == "round" {
precision, parseErr := strconv.Atoi(strings.TrimSpace(operation.Operand))
if parseErr != nil || precision < 0 || precision > 9 {
return false, 0, "", fmt.Errorf("round 小数位必须是 0 到 9 的整数")
}
operand = float64(precision)
}
changed := false
matched := 0
for _, section := range targets {
if operation.TokenIndex < 0 || operation.TokenIndex >= len(section.directTokens) {
continue
}
tokenIndex := section.directTokens[operation.TokenIndex]
current, ok := batchNumericValue(script.tokens[tokenIndex])
if !ok {
return false, 0, "", fmt.Errorf("section [%s] 的第 %d 个值不是数字", normalizeBatchSectionName(operation.Section), operation.TokenIndex)
}
value, err := applyBatchNumericOperator(current, operator, operand, operandEnd)
if err != nil {
return false, 0, "", err
}
next, err := batchNumericToken(value, script.tokens[tokenIndex].typ)
if err != nil {
return false, 0, "", err
}
matched++
if next != script.tokens[tokenIndex] {
changed = true
script.tokens[tokenIndex] = next
}
}
if matched == 0 {
return false, 0, fmt.Sprintf("section [%s] 没有第 %d 个直接值", normalizeBatchSectionName(operation.Section), operation.TokenIndex), nil
}
if matched < len(targets) {
return changed, matched, fmt.Sprintf("section [%s] 有 %d 个重复项缺少第 %d 个直接值", normalizeBatchSectionName(operation.Section), len(targets)-matched, operation.TokenIndex), nil
}
return changed, matched, "", nil
}
func batchNumericValue(token batchToken) (float64, bool) {
switch token.typ {
case 0:
return float64(token.value), true
case 2:
return float64(math.Float32frombits(uint32(token.value))), true
default:
return 0, false
}
}
func applyBatchNumericOperator(current float64, operator string, operand, operandEnd float64) (float64, error) {
var result float64
switch operator {
case "=":
result = operand
case "+":
result = current + operand
case "-":
result = current - operand
case "×", "*":
result = current * operand
case "÷", "/":
if operand == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
result = current / operand
case "+%":
result = current * (1 + operand/100)
case "-%":
result = current * (1 - operand/100)
case "min":
result = math.Min(current, operand)
case "max":
result = math.Max(current, operand)
case "clamp":
result = math.Min(math.Max(current, operand), operandEnd)
case "round":
factor := math.Pow10(int(operand))
result = math.Round(current*factor) / factor
default:
return 0, fmt.Errorf("未知数值运算符: %s", operator)
}
if math.IsNaN(result) || math.IsInf(result, 0) {
return 0, fmt.Errorf("数值运算结果溢出")
}
return result, nil
}
func batchNumericToken(value float64, originalType byte) (batchToken, error) {
if math.IsNaN(value) || math.IsInf(value, 0) {
return batchToken{}, fmt.Errorf("数值运算结果非法")
}
if originalType == 0 && math.Trunc(value) == value && value >= math.MinInt32 && value <= math.MaxInt32 {
return batchToken{typ: 0, value: int32(value)}, nil
}
f := float32(value)
if math.IsInf(float64(f), 0) || math.IsNaN(float64(f)) {
return batchToken{}, fmt.Errorf("数值运算结果超出 float32 范围")
}
return batchToken{typ: 2, value: int32(math.Float32bits(f))}, nil
}
func applyBatchDelete(script *batchScript, operation StructuredBatchOperation) (bool, int, string, error) {
targets := script.matchingSections(operation.Section)
if len(targets) == 0 {
return false, 0, fmt.Sprintf("缺少 section [%s]", normalizeBatchSectionName(operation.Section)), nil
}
type span struct{ start, end int }
spans := make([]span, 0, len(targets))
for _, section := range targets {
end := section.endExclusive
if end <= section.open {
end = section.open + 1
}
spans = append(spans, span{start: section.open, end: end})
}
sort.Slice(spans, func(left, right int) bool {
if spans[left].start != spans[right].start {
return spans[left].start < spans[right].start
}
return spans[left].end > spans[right].end
})
merged := make([]span, 0, len(spans))
for _, item := range spans {
if len(merged) > 0 && item.start < merged[len(merged)-1].end {
if item.end > merged[len(merged)-1].end {
merged[len(merged)-1].end = item.end
}
continue
}
merged = append(merged, item)
}
for index := len(merged) - 1; index >= 0; index-- {
item := merged[index]
script.tokens = append(script.tokens[:item.start], script.tokens[item.end:]...)
}
return true, len(merged), "", nil
}
func (a *Archive) applyBatchInsert(script *batchScript, operation StructuredBatchOperation) (bool, int, string, error) {
sectionName := normalizeBatchSectionName(operation.Section)
if sectionName == "" {
return false, 0, "", fmt.Errorf("插入 section 名称不能为空")
}
values, err := a.parseBatchValue(operation.Value, true, true)
if err != nil {
return false, 0, "", err
}
insertAt := len(script.tokens)
if strings.TrimSpace(operation.AnchorSection) != "" {
anchors := script.matchingSections(operation.AnchorSection)
if len(anchors) == 0 {
return false, 0, fmt.Sprintf("缺少锚点 section [%s]", normalizeBatchSectionName(operation.AnchorSection)), nil
}
insertAt = anchors[0].endExclusive
}
inserted := make([]batchToken, 0, len(values)+2)
inserted = append(inserted, batchToken{typ: 3, value: a.StringOffset("[" + sectionName + "]")})
inserted = append(inserted, values...)
if operation.HasEndTag {
inserted = append(inserted, batchToken{typ: 3, value: a.StringOffset("[/" + sectionName + "]")})
}
updated := make([]batchToken, 0, len(script.tokens)+len(inserted))
updated = append(updated, script.tokens[:insertAt]...)
updated = append(updated, inserted...)
updated = append(updated, script.tokens[insertAt:]...)
if _, err := a.parseBatchScript(encodeBatchTokens(updated)); err != nil {
return false, 0, "", fmt.Errorf("插入值中的 section 结构无效: %w", err)
}
script.tokens = updated
return true, 1, "", nil
}
// cloneForBatch creates an isolated archive state. It shares immutable source
// bytes, but copies every mutable pool, overlay and index so preview can never
// alter the live archive.
func (a *Archive) cloneForBatch() *Archive {
a.cacheMu.Lock()
defer a.cacheMu.Unlock()
clone := &Archive{
textEncoding: a.textEncoding,
data: a.data,
contentDigest: a.contentDigest,
tw: a.tw,
act60: a.act60,
act60Sources: maps.Clone(a.act60Sources),
hdr: a.hdr,
guard: a.guard,
keys: a.keys,
format: a.format,
pageKeys: append([]byte(nil), a.pageKeys...),
sourcePath: a.sourcePath,
tableOff: a.tableOff,
hashOff: a.hashOff,
nameOff: a.nameOff,
grpiOff: a.grpiOff,
bodyOff: a.bodyOff,
hashSize: a.hashSize,
nameSize: a.nameSize,
grpiSize: a.grpiSize,
items: append([]fileItem(nil), a.items...),
groups: append([]groupItem(nil), a.groups...),
strA: append([]byte(nil), a.strA...),
strW: append([]byte(nil), a.strW...),
strAIdx: cloneStringOffsetMap(a.strAIdx),
strWIdx: cloneStringOffsetMap(a.strWIdx),
poolsDirty: a.poolsDirty,
resolveCache: cloneStringMap(a.resolveCache),
resolveCacheOrder: nil,
resolveCacheBytes: 0,
chunkCache: make(map[int32][]byte),
chunkCacheMeta: make(map[int32]chunkCacheEntry),
overlay: cloneBytesMap(a.overlay),
pathIndex: cloneInt32Map(a.pathIndex),
compactPaths: a.compactPaths,
structuralDirty: a.structuralDirty,
removedSpans: cloneRemovedSpans(a.removedSpans),
scriptRenderer: a.scriptRenderer,
canonicalScriptRenderer: a.canonicalScriptRenderer,
}
return clone
}
// CloneForBatch returns an isolated mutable archive state for preview/staging.
func (a *Archive) CloneForBatch() *Archive { return a.cloneForBatch() }
func cloneStringOffsetMap(values map[string]int32) map[string]int32 {
if values == nil {
return nil
}
result := make(map[string]int32, len(values))
for key, value := range values {
result[key] = value
}
return result
}
func cloneStringMap(values map[int32]string) map[int32]string {
if values == nil {
return map[int32]string{}
}
result := make(map[int32]string, len(values))
for key, value := range values {
result[key] = value
}
return result
}
func cloneBytesMap(values map[int32][]byte) map[int32][]byte {
result := make(map[int32][]byte, len(values))
for key, value := range values {
result[key] = append([]byte(nil), value...)
}
return result
}
func cloneInt32Map(values map[string]int32) map[string]int32 {
if values == nil {
return nil
}
result := make(map[string]int32, len(values))
for key, value := range values {
result[key] = value
}
return result
}
// commitBatch copies staged string pools and selected payloads into the live
// archive. The caller must hold the service write lock and must validate every
// selected index before calling this method.
func (a *Archive) commitBatch(stage *Archive, selected map[int32]struct{}) error {
if stage == nil {
return fmt.Errorf("批处理暂存状态为空")
}
nextOverlay := cloneBytesMap(a.overlay)
changed := make([]int32, 0, len(selected))
for index := range selected {
payload, ok := stage.overlay[index]
if !ok {
return fmt.Errorf("批处理文件 %d 缺少暂存 payload", index)
}
previous, err := a.RawBytes(index)
if err != nil {
return err
}
if !bytes.Equal(previous, payload) {
changed = append(changed, index)
}
nextOverlay[index] = append([]byte(nil), payload...)
}
if a.act60 != nil {
for index := range selected {
identity := a.FileIdentity(index)
if text, ok := stage.act60Sources[identity]; ok {
if a.act60Sources == nil {
a.act60Sources = make(map[uint64]string)
}
a.act60Sources[identity] = text
} else {
delete(a.act60Sources, identity)
}
}
}
a.cacheMu.Lock()
a.strA = append([]byte(nil), stage.strA...)
a.strW = append([]byte(nil), stage.strW...)
a.strAIdx = cloneStringOffsetMap(stage.strAIdx)
a.strWIdx = cloneStringOffsetMap(stage.strWIdx)
a.poolsDirty = stage.poolsDirty
a.resolveCache = cloneStringMap(stage.resolveCache)
a.resolveCacheOrder = nil
a.resolveCacheBytes = 0
a.overlay = nextOverlay
a.cacheMu.Unlock()
for _, index := range changed {
a.recordMutation(index, a.Path(index), MutationModified)
}
return nil
}
// CommitBatch atomically copies staged pools and selected file payloads into
// the receiver. Callers must serialize this operation with archive access.
func (a *Archive) CommitBatch(stage *Archive, selected map[int32]struct{}) error {
return a.commitBatch(stage, selected)
}