服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
699 lines
19 KiB
Go
699 lines
19 KiB
Go
package script
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"pvfine/internal/pvf"
|
|
)
|
|
|
|
// BatchAPI is the Go-side host facade used by GojaRuntime. It owns no live
|
|
// archive; all mutations flow through tx.Stage().
|
|
type BatchAPI struct {
|
|
ctx context.Context
|
|
tx *Transaction
|
|
|
|
logFn func(LogEntry)
|
|
progressFn func(Progress)
|
|
|
|
// fileSets is the baseline snapshot plus staged mutations for the user's
|
|
// file sets. It stays nil when the service did not supply one, which makes
|
|
// every file set call fail rather than silently operate on nothing.
|
|
fileSets *FileSetStage
|
|
|
|
scannedFiles int
|
|
}
|
|
|
|
// NewBatchAPI creates a host facade for one transaction.
|
|
func NewBatchAPI(ctx context.Context, tx *Transaction, logFn func(LogEntry), progressFn func(Progress)) *BatchAPI {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return &BatchAPI{ctx: ctx, tx: tx, logFn: logFn, progressFn: progressFn}
|
|
}
|
|
|
|
// SetFileSetStage attaches the file set snapshot and staging area for one run.
|
|
func (a *BatchAPI) SetFileSetStage(stage *FileSetStage) {
|
|
if a == nil {
|
|
return
|
|
}
|
|
a.fileSets = stage
|
|
}
|
|
|
|
// FileSetStage returns the attached staging area, or nil when file sets are
|
|
// unavailable for this run.
|
|
func (a *BatchAPI) FileSetStage() *FileSetStage {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
return a.fileSets
|
|
}
|
|
|
|
func (a *BatchAPI) checkContext() error {
|
|
if a == nil || a.tx == nil || a.tx.Stage() == nil {
|
|
return fmt.Errorf("脚本事务不可用")
|
|
}
|
|
select {
|
|
case <-a.ctx.Done():
|
|
return a.ctx.Err()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// GetOriStrValue resolves references against the staged string tables, keeping
|
|
// legacy literal strings and unresolved references unchanged.
|
|
func (a *BatchAPI) GetOriStrValue(reference string) (string, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return "", err
|
|
}
|
|
if table, key, ok := pvf.ParsePlaceholder(reference); ok {
|
|
if text, found := a.tx.Stage().LookupStringTableText(table, key); found {
|
|
return text, nil
|
|
}
|
|
}
|
|
return a.tx.Stage().ResolvePlaceholders(reference), nil
|
|
}
|
|
|
|
// Files returns every archive entry in deterministic path order.
|
|
func (a *BatchAPI) Files() ([]*FileHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
archive := a.tx.Stage()
|
|
entries := make([]*FileHandle, 0, archive.FileCount())
|
|
for index := int32(0); index < archive.FileCount(); index++ {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, a.newFileHandle(archive.Path(index)))
|
|
}
|
|
a.scannedFiles += int(archive.FileCount())
|
|
sort.SliceStable(entries, func(left, right int) bool {
|
|
leftPath := strings.ToLower(entries[left].Path())
|
|
rightPath := strings.ToLower(entries[right].Path())
|
|
if leftPath != rightPath {
|
|
return leftPath < rightPath
|
|
}
|
|
return entries[left].Index() < entries[right].Index()
|
|
})
|
|
return entries, nil
|
|
}
|
|
|
|
// Find resolves one archive entry using the same normalized lookup semantics
|
|
// as pvf.Archive.Find.
|
|
func (a *BatchAPI) Find(filePath string) (*FileHandle, bool, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
index, ok := a.tx.Stage().Find(filePath)
|
|
if !ok {
|
|
return nil, false, nil
|
|
}
|
|
return a.newFileHandle(a.tx.Stage().Path(index)), true, nil
|
|
}
|
|
|
|
// Glob resolves slash-normalized case-insensitive patterns. '*' and '?' do
|
|
// not cross '/', while '**' may cross directory boundaries.
|
|
func (a *BatchAPI) Glob(pattern string) ([]*FileHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
pattern = normalizeGlob(pattern)
|
|
if pattern == "" {
|
|
return nil, fmt.Errorf("glob 模式不能为空")
|
|
}
|
|
archive := a.tx.Stage()
|
|
result := make([]*FileHandle, 0)
|
|
for index := int32(0); index < archive.FileCount(); index++ {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
if globMatch(pattern, normalizeGlob(archive.Path(index))) {
|
|
result = append(result, a.newFileHandle(archive.Path(index)))
|
|
}
|
|
}
|
|
a.scannedFiles += int(archive.FileCount())
|
|
return result, nil
|
|
}
|
|
|
|
func normalizeGlob(value string) string {
|
|
return strings.ToLower(strings.Trim(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"), "/"))
|
|
}
|
|
|
|
func globMatch(pattern, value string) bool {
|
|
patternRunes := []rune(pattern)
|
|
valueRunes := []rune(value)
|
|
states := make([][]bool, len(patternRunes)+1)
|
|
for index := range states {
|
|
states[index] = make([]bool, len(valueRunes)+1)
|
|
}
|
|
states[0][0] = true
|
|
for pi, token := range patternRunes {
|
|
for vi := 0; vi <= len(valueRunes); vi++ {
|
|
if !states[pi][vi] {
|
|
continue
|
|
}
|
|
switch token {
|
|
case '*':
|
|
if pi+1 < len(patternRunes) && patternRunes[pi+1] == '*' {
|
|
if pi+2 < len(patternRunes) && patternRunes[pi+2] == '/' {
|
|
// A recursive directory wildcard may match zero complete
|
|
// path components, including the following slash.
|
|
states[pi+3][vi] = true
|
|
} else {
|
|
states[pi+2][vi] = true
|
|
}
|
|
if vi < len(valueRunes) {
|
|
states[pi][vi+1] = true
|
|
}
|
|
} else {
|
|
states[pi+1][vi] = true
|
|
if vi < len(valueRunes) && valueRunes[vi] != '/' {
|
|
states[pi][vi+1] = true
|
|
}
|
|
}
|
|
case '?':
|
|
if vi < len(valueRunes) && valueRunes[vi] != '/' {
|
|
states[pi+1][vi+1] = true
|
|
}
|
|
default:
|
|
if vi < len(valueRunes) && token == valueRunes[vi] {
|
|
states[pi+1][vi+1] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return states[len(patternRunes)][len(valueRunes)]
|
|
}
|
|
|
|
// ScannedCount reports how many archive entries host queries inspected.
|
|
func (a *BatchAPI) ScannedCount() int {
|
|
if a == nil {
|
|
return 0
|
|
}
|
|
return a.scannedFiles
|
|
}
|
|
|
|
// ModifiedCount reports the number of unique final changes, including created
|
|
// and deleted entries.
|
|
func (a *BatchAPI) ModifiedCount() int {
|
|
if a == nil || a.tx == nil {
|
|
return 0
|
|
}
|
|
changes, err := a.tx.Changes()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return len(changes)
|
|
}
|
|
|
|
// CreateFile stages a new entry and returns its handle. text is the initial
|
|
// decompiled content; an empty text creates an empty file.
|
|
func (a *BatchAPI) CreateFile(filePath string, dataType int32, text string) (*FileHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
path, err := a.tx.CreateFile(filePath, dataType, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if text != "" {
|
|
if err := a.tx.SetText(path, text); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return a.newFileHandle(path), nil
|
|
}
|
|
|
|
// CopyFile stages a copy of an existing entry.
|
|
func (a *BatchAPI) CopyFile(from, to string, overwrite bool) (*FileHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
path, err := a.tx.CopyFile(from, to, overwrite)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.newFileHandle(path), nil
|
|
}
|
|
|
|
// DeleteFile stages removal of one entry.
|
|
func (a *BatchAPI) DeleteFile(filePath string) (bool, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return false, err
|
|
}
|
|
return a.tx.DeleteFile(filePath)
|
|
}
|
|
|
|
// OpenList resolves a .lst file and returns its list facade.
|
|
func (a *BatchAPI) OpenList(filePath string) (*ListHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
index, ok := a.tx.Stage().FindList(filePath)
|
|
if !ok {
|
|
return nil, fmt.Errorf("列表文件不存在: %s", filePath)
|
|
}
|
|
if a.tx.Stage().File(index).DataType != pvf.TypeScript {
|
|
return nil, fmt.Errorf("文件 %s 不是 .lst 列表", a.tx.Stage().Path(index))
|
|
}
|
|
return &ListHandle{api: a, path: a.tx.Stage().Path(index)}, nil
|
|
}
|
|
|
|
// ListHandle is the narrow Go host object behind a JavaScript PVFList. Entry
|
|
// paths follow the archive's list layout: directory-relative for legacy lists,
|
|
// archive-root-relative for Paged110 and centralized list/ lists.
|
|
type ListHandle struct {
|
|
api *BatchAPI
|
|
path string
|
|
}
|
|
|
|
// Path returns the archive path of the list file.
|
|
func (l *ListHandle) Path() string {
|
|
if l == nil {
|
|
return ""
|
|
}
|
|
return l.path
|
|
}
|
|
|
|
// Get returns the id/path pairs in file order.
|
|
func (l *ListHandle) Get() ([]pvf.ListPair, error) {
|
|
if err := l.api.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
return l.api.tx.ListPairs(l.path)
|
|
}
|
|
|
|
// Set inserts or updates one id/path entry.
|
|
func (l *ListHandle) Set(id, entryPath string) error {
|
|
if err := l.api.checkContext(); err != nil {
|
|
return err
|
|
}
|
|
stored, err := l.storagePath(entryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return l.api.tx.SetListPairs(l.path, []pvf.ListPair{{ID: id, Path: stored}})
|
|
}
|
|
|
|
// MSet inserts or updates several id/path entries at once.
|
|
func (l *ListHandle) MSet(pairs []pvf.ListPair) error {
|
|
if err := l.api.checkContext(); err != nil {
|
|
return err
|
|
}
|
|
if len(pairs) == 0 {
|
|
return nil
|
|
}
|
|
resolved := make([]pvf.ListPair, 0, len(pairs))
|
|
for _, pair := range pairs {
|
|
stored, err := l.storagePath(pair.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolved = append(resolved, pvf.ListPair{ID: pair.ID, Path: stored})
|
|
}
|
|
return l.api.tx.SetListPairs(l.path, resolved)
|
|
}
|
|
|
|
// Unset removes every entry whose id matches and reports whether any was
|
|
// removed.
|
|
func (l *ListHandle) Unset(id string) (bool, error) {
|
|
if err := l.api.checkContext(); err != nil {
|
|
return false, err
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return false, fmt.Errorf("列表 id 不能为空")
|
|
}
|
|
removed, err := l.api.tx.UnsetListIDs(l.path, []string{id})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return removed > 0, nil
|
|
}
|
|
|
|
// GetID returns the id registered for entryPath, or false when the list has no
|
|
// matching entry. Unlike Set, it neither validates nor requires the target file
|
|
// to exist: it normalizes the path and compares it with the list content.
|
|
func (l *ListHandle) GetID(entryPath string) (string, bool, error) {
|
|
if err := l.api.checkContext(); err != nil {
|
|
return "", false, err
|
|
}
|
|
raw, err := normalizeListPath(entryPath)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
for _, candidate := range l.candidatePaths(raw) {
|
|
id, found, err := l.api.tx.ListID(l.path, candidate)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
if found {
|
|
return id, true, nil
|
|
}
|
|
}
|
|
return "", false, nil
|
|
}
|
|
|
|
// storagePath resolves an entry path to the native form the .lst file stores.
|
|
// Both full archive paths and list-directory-relative spellings are accepted.
|
|
// The target must exist in the archive so a typo cannot register a dangling
|
|
// entry.
|
|
func (l *ListHandle) storagePath(entryPath string) (string, error) {
|
|
raw, err := normalizeListPath(entryPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, candidate := range l.candidatePaths(raw) {
|
|
if l.targetExists(candidate) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("列表条目指向的文件不存在: %s", raw)
|
|
}
|
|
|
|
// candidatePaths lists native storage spellings, preferring the literal input
|
|
// when both interpretations resolve. Root-relative lists never strip prefixes.
|
|
func (l *ListHandle) candidatePaths(raw string) []string {
|
|
candidates := []string{raw}
|
|
if l.rootRelative() {
|
|
if joined := path.Join(path.Dir(l.path), raw); joined != raw {
|
|
candidates = append(candidates, joined)
|
|
}
|
|
return candidates
|
|
}
|
|
if rebased, ok := l.rebase(raw); ok && rebased != raw {
|
|
candidates = append(candidates, rebased)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func (l *ListHandle) rootRelative() bool {
|
|
return l.api.tx.Stage().IsPaged() || strings.EqualFold(path.Dir(l.path), "list")
|
|
}
|
|
|
|
// rebase converts a full archive path into the list-relative form, when the
|
|
// path lives under the list file's directory.
|
|
func (l *ListHandle) rebase(archivePath string) (string, bool) {
|
|
dir := path.Dir(l.path)
|
|
if dir == "." {
|
|
return archivePath, true
|
|
}
|
|
prefix := strings.ToLower(dir) + "/"
|
|
if !strings.HasPrefix(strings.ToLower(archivePath), prefix) {
|
|
return "", false
|
|
}
|
|
relative := archivePath[len(prefix):]
|
|
if relative == "" {
|
|
return "", false
|
|
}
|
|
return relative, true
|
|
}
|
|
|
|
// targetExists reports whether a native storage path resolves to an archive
|
|
// entry, mirroring the service lookup that also accepts a "(r)" sibling.
|
|
func (l *ListHandle) targetExists(stored string) bool {
|
|
archivePath := stored
|
|
if !l.rootRelative() {
|
|
archivePath = path.Join(path.Dir(l.path), stored)
|
|
}
|
|
if _, ok := l.api.tx.Stage().Find(archivePath); ok {
|
|
return true
|
|
}
|
|
base := path.Base(archivePath)
|
|
if strings.HasPrefix(strings.ToLower(base), "(r)") {
|
|
return false
|
|
}
|
|
_, ok := l.api.tx.Stage().Find(path.Join(path.Dir(archivePath), "(r)"+base))
|
|
return ok
|
|
}
|
|
|
|
// normalizeListPath trims an incoming entry path to its archive spelling.
|
|
func normalizeListPath(entryPath string) (string, error) {
|
|
raw := strings.TrimSpace(strings.ReplaceAll(entryPath, "\\", "/"))
|
|
raw = strings.TrimPrefix(raw, "./")
|
|
raw = strings.Trim(raw, "/")
|
|
if raw == "" {
|
|
return "", fmt.Errorf("列表条目路径不能为空")
|
|
}
|
|
if raw == "." || raw == ".." || strings.HasPrefix(raw, "../") {
|
|
return "", fmt.Errorf("列表条目路径无效: %q", entryPath)
|
|
}
|
|
return raw, nil
|
|
}
|
|
|
|
// OpenFileSet resolves one file set from the run's staged view. A set created
|
|
// earlier in the same run is visible here, because the stage merges the
|
|
// baseline snapshot with pending mutations. found is false for an unknown name,
|
|
// which the script sees as null.
|
|
func (a *BatchAPI) OpenFileSet(name string) (*FileSetHandle, bool, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
if a.fileSets == nil {
|
|
return nil, false, fmt.Errorf("当前运行不支持文件集")
|
|
}
|
|
normalized := NormalizeFileSetName(name)
|
|
if normalized == "" {
|
|
return nil, false, fmt.Errorf("文件集名称不能为空")
|
|
}
|
|
if _, ok := a.fileSets.Lookup(normalized); !ok {
|
|
return nil, false, nil
|
|
}
|
|
return &FileSetHandle{api: a, name: normalized}, true, nil
|
|
}
|
|
|
|
// CreateFileSet stages a new file set with the supplied entry paths.
|
|
func (a *BatchAPI) CreateFileSet(name string, paths []string) (*FileSetHandle, error) {
|
|
if err := a.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.fileSets == nil {
|
|
return nil, fmt.Errorf("当前运行不支持文件集")
|
|
}
|
|
fileSet, err := a.fileSets.Create(name, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &FileSetHandle{api: a, name: fileSet.Name}, nil
|
|
}
|
|
|
|
// FileSetHandle is the narrow Go host object behind a JavaScript PVFFileSet.
|
|
type FileSetHandle struct {
|
|
api *BatchAPI
|
|
name string
|
|
}
|
|
|
|
// Name returns the resolved set name.
|
|
func (h *FileSetHandle) Name() string {
|
|
if h == nil {
|
|
return ""
|
|
}
|
|
return h.name
|
|
}
|
|
|
|
// Get returns the staged entry paths in stored order.
|
|
func (h *FileSetHandle) Get() ([]string, error) {
|
|
fileSet, err := h.current()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paths := make([]string, 0, len(fileSet.Entries))
|
|
for _, entry := range fileSet.Entries {
|
|
paths = append(paths, entry.Path)
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
// Set replaces the entire entry list, preserving metadata of surviving paths.
|
|
func (h *FileSetHandle) Set(paths []string) (int, error) {
|
|
if err := h.api.checkContext(); err != nil {
|
|
return 0, err
|
|
}
|
|
fileSet, err := h.api.fileSets.Replace(h.name, paths)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(fileSet.Entries), nil
|
|
}
|
|
|
|
// current re-resolves the set on every call: an earlier call in the same run
|
|
// may have replaced or created it after this handle was captured.
|
|
func (h *FileSetHandle) current() (FileSet, error) {
|
|
if h == nil || h.api == nil {
|
|
return FileSet{}, fmt.Errorf("文件集句柄无效")
|
|
}
|
|
if err := h.api.checkContext(); err != nil {
|
|
return FileSet{}, err
|
|
}
|
|
if h.api.fileSets == nil {
|
|
return FileSet{}, fmt.Errorf("当前运行不支持文件集")
|
|
}
|
|
fileSet, ok := h.api.fileSets.Lookup(h.name)
|
|
if !ok {
|
|
return FileSet{}, fmt.Errorf("文件集不存在: %s", h.name)
|
|
}
|
|
return fileSet, nil
|
|
}
|
|
|
|
// Log records a message and forwards it to the service event sink.
|
|
func (a *BatchAPI) Log(level, message string) {
|
|
if a == nil || a.logFn == nil {
|
|
return
|
|
}
|
|
a.logFn(LogEntry{Level: level, Message: message})
|
|
}
|
|
|
|
// Progress forwards a user-provided progress update.
|
|
func (a *BatchAPI) Progress(done, total int, message string) error {
|
|
if err := a.checkContext(); err != nil {
|
|
return err
|
|
}
|
|
if done < 0 || total < 0 || (total > 0 && done > total) {
|
|
return fmt.Errorf("进度范围无效: %d/%d", done, total)
|
|
}
|
|
if a.progressFn != nil {
|
|
a.progressFn(Progress{Done: done, Total: total, Message: message})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *BatchAPI) newFileHandle(path string) *FileHandle {
|
|
return &FileHandle{api: a, path: path, deleteRevision: a.tx.DeleteRevision()}
|
|
}
|
|
|
|
// FileHandle is the narrow Go host object behind a JavaScript PVFFile. It is
|
|
// addressed by path because staged creates and deletes shift entry indexes.
|
|
type FileHandle struct {
|
|
api *BatchAPI
|
|
path string
|
|
deleteRevision int
|
|
}
|
|
|
|
func (f *FileHandle) Index() int32 {
|
|
index, ok := f.lookup()
|
|
if !ok {
|
|
return -1
|
|
}
|
|
return index
|
|
}
|
|
|
|
func (f *FileHandle) Path() string {
|
|
if f == nil {
|
|
return ""
|
|
}
|
|
return f.path
|
|
}
|
|
|
|
func (f *FileHandle) Type() int32 {
|
|
index, ok := f.lookup()
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return f.api.tx.Stage().File(index).DataType
|
|
}
|
|
|
|
func (f *FileHandle) Size() int32 {
|
|
index, ok := f.lookup()
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return f.api.tx.Stage().File(index).DataSize
|
|
}
|
|
|
|
// lookup resolves the handle and rejects handles captured before a staged
|
|
// removal, because that removal may have renumbered the entry.
|
|
func (f *FileHandle) lookup() (int32, bool) {
|
|
if f == nil || f.api == nil || f.api.tx == nil || f.api.tx.Stage() == nil {
|
|
return 0, false
|
|
}
|
|
if f.deleteRevision != f.api.tx.DeleteRevision() {
|
|
return 0, false
|
|
}
|
|
return f.api.tx.Stage().Find(f.path)
|
|
}
|
|
|
|
// staleError explains why a handle is unusable.
|
|
func (f *FileHandle) staleError() error {
|
|
if f != nil && f.api != nil && f.api.tx != nil && f.deleteRevision != f.api.tx.DeleteRevision() {
|
|
return fmt.Errorf("文件 %s 的句柄已失效,请重新查询后再操作", f.path)
|
|
}
|
|
return fmt.Errorf("文件不存在: %s", f.Path())
|
|
}
|
|
|
|
func (f *FileHandle) Text() (string, error) {
|
|
if err := f.api.checkContext(); err != nil {
|
|
return "", err
|
|
}
|
|
index, ok := f.lookup()
|
|
if !ok {
|
|
return "", f.staleError()
|
|
}
|
|
return f.api.tx.Stage().Text(index)
|
|
}
|
|
|
|
func (f *FileHandle) SetText(text string) error {
|
|
if err := f.api.checkContext(); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := f.lookup(); !ok {
|
|
return f.staleError()
|
|
}
|
|
return f.api.tx.SetText(f.path, text)
|
|
}
|
|
|
|
func (f *FileHandle) Parse() (*pvf.ScriptDocument, error) {
|
|
if err := f.api.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
index, ok := f.lookup()
|
|
if !ok {
|
|
return nil, f.staleError()
|
|
}
|
|
if f.Type() != pvf.TypeScript {
|
|
return nil, fmt.Errorf("文件 %s 不是 TypeScript", f.Path())
|
|
}
|
|
raw, err := f.api.tx.Stage().RawBytes(index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document, err := f.api.tx.Stage().ParseScriptDocument(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, warning := range document.Warnings() {
|
|
f.api.Log(
|
|
LogLevelWarn,
|
|
fmt.Sprintf("%s: [%s] %s", f.Path(), warning.Code, warning.Message),
|
|
)
|
|
}
|
|
return document, nil
|
|
}
|
|
|
|
func (f *FileHandle) Write(document *pvf.ScriptDocument) error {
|
|
if err := f.api.checkContext(); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := f.lookup(); !ok {
|
|
return f.staleError()
|
|
}
|
|
if f.Type() != pvf.TypeScript {
|
|
return fmt.Errorf("文件 %s 不是 TypeScript", f.Path())
|
|
}
|
|
raw, err := f.api.tx.Stage().EncodeScriptDocument(document)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return f.api.tx.SetRawBytes(f.path, raw)
|
|
}
|
|
|
|
// PathPatternMatch is exported for focused unit tests and future callers that
|
|
// need to validate the same virtual path pattern grammar.
|
|
func PathPatternMatch(pattern, value string) bool {
|
|
return globMatch(normalizeGlob(pattern), normalizeGlob(value))
|
|
}
|