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,695 @@
|
||||
package pvf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListPair is one id/path pair from a .lst script.
|
||||
type ListPair struct {
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// listRecord is one decoded id/path token pair together with its original
|
||||
// tokens. Unchanged records are re-emitted verbatim so token encoding and
|
||||
// string-pool offsets stay stable across an edit.
|
||||
type listRecord struct {
|
||||
id string
|
||||
path string
|
||||
idToken batchToken
|
||||
pathToken batchToken
|
||||
}
|
||||
|
||||
// ScriptImageReference is the raw PVF representation used by [icon] and
|
||||
// [field image]. Services map it to their public ImageReference type.
|
||||
type ScriptImageReference struct {
|
||||
Path string `json:"path"`
|
||||
Index int32 `json:"index"`
|
||||
}
|
||||
|
||||
// StringTableReference identifies one placeholder used by a script metadata
|
||||
// field. Services use it to invalidate only registered records whose display
|
||||
// metadata actually depends on a changed localization file.
|
||||
type StringTableReference struct {
|
||||
Index int `json:"index"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// ScriptMetadata is the small metadata projection shared by name/search and
|
||||
// image rendering. It intentionally does not decompile the full script.
|
||||
type ScriptMetadata struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
HasName bool `json:"-"`
|
||||
Icon *ScriptImageReference `json:"icon,omitempty"`
|
||||
FieldImage *ScriptImageReference `json:"fieldImage,omitempty"`
|
||||
StringTableReferences []StringTableReference `json:"-"`
|
||||
|
||||
// Rarity is the raw value of the first [rarity] section, which the
|
||||
// frontend maps to a display color (equipment and stackables share the
|
||||
// same 0..6 scale). HasRarity reports whether the section exists at all.
|
||||
Rarity int32 `json:"rarity"`
|
||||
HasRarity bool `json:"-"`
|
||||
|
||||
// NameFallback reports that the name's `<table::key>` placeholder was
|
||||
// answered by a language overlay because the archive's own localization has
|
||||
// no text for that key, so a caller that displays the name can flag it.
|
||||
NameFallback bool `json:"-"`
|
||||
}
|
||||
|
||||
// RarityUnknown is the sentinel used by callers that project Rarity into a
|
||||
// plain int32: it means the file declares no [rarity] section.
|
||||
const RarityUnknown = -1
|
||||
|
||||
// RarityValue returns Rarity, or RarityUnknown when the script has no
|
||||
// [rarity] section, so callers never mistake an absent value for "普通".
|
||||
func (m ScriptMetadata) RarityValue() int32 {
|
||||
if !m.HasRarity {
|
||||
return RarityUnknown
|
||||
}
|
||||
return m.Rarity
|
||||
}
|
||||
|
||||
// ScriptListPairs parses a TypeScript .lst payload as consecutive id/path
|
||||
// token pairs. The .lst line layout is presentation-only, so token pairs are
|
||||
// used instead of trying to recover source lines from the binary payload.
|
||||
func (a *Archive) ScriptListPairs(i int32) ([]ListPair, error) {
|
||||
if i < 0 || i >= int32(len(a.items)) {
|
||||
return nil, ErrBadIndex
|
||||
}
|
||||
if a.items[i].typ != TypeScript {
|
||||
return nil, fmt.Errorf("pvf: file %d is not a script", i)
|
||||
}
|
||||
raw, err := a.RawBytes(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(raw)%5 != 0 {
|
||||
return nil, fmt.Errorf("pvf: malformed script payload for file %d", i)
|
||||
}
|
||||
|
||||
values := make([]scriptMetadataValue, 0, len(raw)/5)
|
||||
invalid := false
|
||||
for pos := 0; pos < len(raw); pos += 5 {
|
||||
value, ok := a.scriptMetadataValue(raw[pos], int32(binary.LittleEndian.Uint32(raw[pos+1:])))
|
||||
if !ok {
|
||||
invalid = true
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
if invalid || len(values)%2 != 0 {
|
||||
return nil, fmt.Errorf("pvf: incomplete or unsupported .lst pair data for file %d", i)
|
||||
}
|
||||
|
||||
pairs := make([]ListPair, 0, len(values)/2)
|
||||
for pos := 0; pos+1 < len(values); pos += 2 {
|
||||
id := strings.TrimSpace(values[pos].text)
|
||||
path := strings.TrimSpace(values[pos+1].text)
|
||||
if id == "" || path == "" {
|
||||
continue
|
||||
}
|
||||
pairs = append(pairs, ListPair{ID: id, Path: path})
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// RemoveListPairs removes matching id/path pairs from a TypeScript .lst
|
||||
// payload while preserving the original token encoding of every remaining
|
||||
// pair. Duplicate matching pairs are removed together.
|
||||
func (a *Archive) RemoveListPairs(i int32, entries []ListPair) (int, error) {
|
||||
if i < 0 || i >= int32(len(a.items)) {
|
||||
return 0, ErrBadIndex
|
||||
}
|
||||
if a.items[i].typ != TypeScript {
|
||||
return 0, fmt.Errorf("pvf: file %d is not a script", i)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
targets := make(map[string]struct{}, len(entries))
|
||||
for _, entry := range entries {
|
||||
id := strings.TrimSpace(entry.ID)
|
||||
path := strings.TrimSpace(entry.Path)
|
||||
if id == "" || path == "" {
|
||||
continue
|
||||
}
|
||||
targets[id+"\x00"+path] = struct{}{}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
raw, err := a.RawBytes(i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(raw)%5 != 0 {
|
||||
return 0, fmt.Errorf("pvf: malformed script payload for file %d", i)
|
||||
}
|
||||
values := make([]scriptMetadataValue, 0, len(raw)/5)
|
||||
for pos := 0; pos < len(raw); pos += 5 {
|
||||
value, ok := a.scriptMetadataValue(raw[pos], int32(binary.LittleEndian.Uint32(raw[pos+1:])))
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("pvf: incomplete or unsupported .lst pair data for file %d", i)
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
if len(values)%2 != 0 {
|
||||
return 0, fmt.Errorf("pvf: incomplete or unsupported .lst pair data for file %d", i)
|
||||
}
|
||||
|
||||
result := make([]byte, 0, len(raw))
|
||||
removed := 0
|
||||
for pos := 0; pos < len(values); pos += 2 {
|
||||
id := strings.TrimSpace(values[pos].text)
|
||||
path := strings.TrimSpace(values[pos+1].text)
|
||||
if _, ok := targets[id+"\x00"+path]; ok && id != "" && path != "" {
|
||||
removed++
|
||||
continue
|
||||
}
|
||||
result = append(result, raw[pos*5:(pos+2)*5]...)
|
||||
}
|
||||
if removed == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if err := a.SetRawBytes(i, result); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
// decodeListRecords decodes a .lst payload into records. Every token must be
|
||||
// a valid id/path member and the token count must be even.
|
||||
func (a *Archive) decodeListRecords(i int32, raw []byte) ([]listRecord, error) {
|
||||
if len(raw)%5 != 0 {
|
||||
return nil, fmt.Errorf("pvf: malformed script payload for file %d", i)
|
||||
}
|
||||
tokens := make([]batchToken, 0, len(raw)/5)
|
||||
values := make([]scriptMetadataValue, 0, len(raw)/5)
|
||||
for pos := 0; pos < len(raw); pos += 5 {
|
||||
value, ok := a.scriptMetadataValue(raw[pos], int32(binary.LittleEndian.Uint32(raw[pos+1:])))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("pvf: incomplete or unsupported .lst pair data for file %d", i)
|
||||
}
|
||||
tokens = append(tokens, batchToken{
|
||||
typ: raw[pos],
|
||||
value: int32(binary.LittleEndian.Uint32(raw[pos+1:])),
|
||||
})
|
||||
values = append(values, value)
|
||||
}
|
||||
if len(values)%2 != 0 {
|
||||
return nil, fmt.Errorf("pvf: incomplete or unsupported .lst pair data for file %d", i)
|
||||
}
|
||||
|
||||
records := make([]listRecord, 0, len(values)/2)
|
||||
for pos := 0; pos+1 < len(values); pos += 2 {
|
||||
records = append(records, listRecord{
|
||||
id: strings.TrimSpace(values[pos].text),
|
||||
path: strings.TrimSpace(values[pos+1].text),
|
||||
idToken: tokens[pos],
|
||||
pathToken: tokens[pos+1],
|
||||
})
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// listPathToken encodes a .lst entry path as the backtick-quoted string token
|
||||
// used by real list files.
|
||||
func (a *Archive) listPathToken(path string) batchToken {
|
||||
return batchToken{typ: 6, value: a.StringOffset(path)}
|
||||
}
|
||||
|
||||
// listIDToken encodes a list id. Numeric ids keep the integer token used by
|
||||
// real .lst files; anything else falls back to a quoted string.
|
||||
func (a *Archive) listIDToken(id string) batchToken {
|
||||
if number, err := strconv.ParseInt(id, 10, 32); err == nil {
|
||||
return batchToken{typ: 0, value: int32(number)}
|
||||
}
|
||||
return batchToken{typ: 6, value: a.StringOffset(id)}
|
||||
}
|
||||
|
||||
func encodeListRecords(records []listRecord) []byte {
|
||||
raw := make([]byte, 0, len(records)*10)
|
||||
for _, record := range records {
|
||||
raw = append(raw, encodeBatchTokens([]batchToken{record.idToken, record.pathToken})...)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// ListPairs returns the id/path pairs of a .lst payload in file order. When an
|
||||
// id repeats, the first occurrence wins.
|
||||
func (a *Archive) ListPairs(i int32) ([]ListPair, error) {
|
||||
records, err := a.listRecordsAt(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := make(map[string]struct{}, len(records))
|
||||
pairs := make([]ListPair, 0, len(records))
|
||||
for _, record := range records {
|
||||
if record.id == "" || record.path == "" {
|
||||
continue
|
||||
}
|
||||
if _, duplicate := seen[record.id]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[record.id] = struct{}{}
|
||||
pairs = append(pairs, ListPair{ID: record.id, Path: record.path})
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func (a *Archive) listRecordsAt(i int32) ([]listRecord, error) {
|
||||
if i < 0 || i >= int32(len(a.items)) {
|
||||
return nil, ErrBadIndex
|
||||
}
|
||||
if a.items[i].typ != TypeScript {
|
||||
return nil, fmt.Errorf("pvf: file %d is not a script", i)
|
||||
}
|
||||
raw, err := a.RawBytes(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.decodeListRecords(i, raw)
|
||||
}
|
||||
|
||||
// SetListPairs inserts or updates id/path pairs in a .lst payload. An existing
|
||||
// id keeps its position and id-token encoding while its path is replaced; a
|
||||
// new id is appended. Duplicate records for an id are collapsed into the first
|
||||
// one, so an id addresses exactly one entry afterwards.
|
||||
func (a *Archive) SetListPairs(i int32, pairs []ListPair) error {
|
||||
if len(pairs) == 0 {
|
||||
return nil
|
||||
}
|
||||
records, err := a.listRecordsAt(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Match by trimmed id, mirroring how ListPairs reads the file. The first
|
||||
// occurrence wins, so duplicates for the same id collapse into it.
|
||||
firstByID := make(map[string]int, len(records))
|
||||
for position, record := range records {
|
||||
if _, exists := firstByID[record.id]; !exists {
|
||||
firstByID[record.id] = position
|
||||
}
|
||||
}
|
||||
|
||||
requested := make(map[string]listRecord, len(pairs))
|
||||
appended := make([]listRecord, 0, len(pairs))
|
||||
for _, pair := range pairs {
|
||||
id := strings.TrimSpace(pair.ID)
|
||||
path := strings.TrimSpace(pair.Path)
|
||||
if id == "" || path == "" {
|
||||
return fmt.Errorf("pvf: 列表条目的 id 和路径不能为空")
|
||||
}
|
||||
record := listRecord{
|
||||
id: id,
|
||||
path: path,
|
||||
idToken: a.listIDToken(id),
|
||||
pathToken: a.listPathToken(path),
|
||||
}
|
||||
position, exists := firstByID[id]
|
||||
if !exists {
|
||||
firstByID[id] = len(records) + len(appended)
|
||||
appended = append(appended, record)
|
||||
continue
|
||||
}
|
||||
// An updated entry keeps the original id token so its encoding and
|
||||
// string-pool offset are untouched.
|
||||
record.idToken = records[position].idToken
|
||||
requested[id] = record
|
||||
}
|
||||
|
||||
updated := make([]listRecord, 0, len(records)+len(appended))
|
||||
for position, record := range records {
|
||||
// A duplicate of an id being set is dropped; the surviving copy is the
|
||||
// first occurrence, with its replacement applied if there is one.
|
||||
if _, isSet := requested[record.id]; isSet && firstByID[record.id] != position {
|
||||
continue
|
||||
}
|
||||
if replacement, ok := requested[record.id]; ok && firstByID[record.id] == position {
|
||||
updated = append(updated, replacement)
|
||||
continue
|
||||
}
|
||||
updated = append(updated, record)
|
||||
}
|
||||
updated = append(updated, appended...)
|
||||
return a.SetRawBytes(i, encodeListRecords(updated))
|
||||
}
|
||||
|
||||
// SetListPair inserts or updates one id/path pair. See SetListPairs.
|
||||
func (a *Archive) SetListPair(i int32, id, path string) error {
|
||||
return a.SetListPairs(i, []ListPair{{ID: id, Path: path}})
|
||||
}
|
||||
|
||||
// RemoveListIDs removes every record whose id matches, returning the number of
|
||||
// removed records. Other records are re-emitted verbatim.
|
||||
func (a *Archive) RemoveListIDs(i int32, ids []string) (int, error) {
|
||||
targets := make(map[string]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
targets[id] = struct{}{}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
records, err := a.listRecordsAt(i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
kept := make([]listRecord, 0, len(records))
|
||||
removed := 0
|
||||
for _, record := range records {
|
||||
if _, ok := targets[record.id]; ok && record.id != "" {
|
||||
removed++
|
||||
continue
|
||||
}
|
||||
kept = append(kept, record)
|
||||
}
|
||||
if removed == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if err := a.SetRawBytes(i, encodeListRecords(kept)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
// ListID returns the id registered for path. It reports a malformed payload
|
||||
// rather than treating it as "not found", so a lookup failure cannot be
|
||||
// mistaken for an absent entry.
|
||||
func (a *Archive) ListID(i int32, path string) (string, bool, error) {
|
||||
records, err := a.listRecordsAt(i)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
for _, record := range records {
|
||||
if record.path == path && record.id != "" {
|
||||
return record.id, true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// ScriptMetadata extracts [name], [icon], [field image] and [rarity] in one
|
||||
// raw token scan without formatting or materializing the full decompiled
|
||||
// script.
|
||||
//
|
||||
// The name is display text: type 8/10 string-pool references are handled like
|
||||
// the older 5/6/7 ones, and `<table::key>` placeholders (which is what the
|
||||
// newer clients store instead of the text itself) are resolved through the
|
||||
// archive's string tables so search and the explorer can match the text the
|
||||
// player sees.
|
||||
func (a *Archive) ScriptMetadata(i int32) (ScriptMetadata, error) {
|
||||
if i < 0 || i >= int32(len(a.items)) {
|
||||
return ScriptMetadata{}, ErrBadIndex
|
||||
}
|
||||
if a.items[i].typ != TypeScript {
|
||||
return ScriptMetadata{}, fmt.Errorf("pvf: file %d is not a script", i)
|
||||
}
|
||||
raw, err := a.RawBytes(i)
|
||||
if err != nil {
|
||||
return ScriptMetadata{}, err
|
||||
}
|
||||
if len(raw)%5 != 0 {
|
||||
return ScriptMetadata{}, fmt.Errorf("pvf: malformed script payload for file %d", i)
|
||||
}
|
||||
|
||||
type metadataToken struct {
|
||||
typ byte
|
||||
value int32
|
||||
text string
|
||||
}
|
||||
type metadataSection struct {
|
||||
name string
|
||||
kind string
|
||||
depth int
|
||||
values []metadataToken
|
||||
}
|
||||
|
||||
// PVF scripts contain both paired sections and legacy, unpaired sections.
|
||||
// First collect closing names so the stack follows the same semantic rule
|
||||
// as decodeScript and ParseScriptView.
|
||||
type sectionTag struct {
|
||||
name string
|
||||
closing, valid bool
|
||||
}
|
||||
// Resolve each tag offset once per extraction. This remains local so edits,
|
||||
// pool replacement and concurrent readers cannot observe stale tag identities.
|
||||
tags := make(map[int32]sectionTag)
|
||||
pairedNames := make(map[string]bool)
|
||||
for pos := 0; pos < len(raw); pos += 5 {
|
||||
if raw[pos] != 3 {
|
||||
continue
|
||||
}
|
||||
offset := int32(binary.LittleEndian.Uint32(raw[pos+1:]))
|
||||
tag, exists := tags[offset]
|
||||
if !exists {
|
||||
tag.name, tag.closing, tag.valid = parseSectionTag(a.ResolveString(offset))
|
||||
tags[offset] = tag
|
||||
}
|
||||
if tag.valid && tag.closing {
|
||||
pairedNames[tag.name] = true
|
||||
}
|
||||
}
|
||||
|
||||
sections := make([]metadataSection, 0)
|
||||
stack := make([]int, 0)
|
||||
activeUnpaired := make(map[int]int)
|
||||
closeUnpaired := func(depth int) {
|
||||
delete(activeUnpaired, depth)
|
||||
}
|
||||
for pos := 0; pos < len(raw); pos += 5 {
|
||||
typ := raw[pos]
|
||||
value := int32(binary.LittleEndian.Uint32(raw[pos+1:]))
|
||||
if typ == 3 {
|
||||
tag := tags[value]
|
||||
name, closing := tag.name, tag.closing
|
||||
if tag.valid {
|
||||
depth := len(stack)
|
||||
closeUnpaired(depth)
|
||||
if closing {
|
||||
for position := len(stack) - 1; position >= 0; position-- {
|
||||
if sections[stack[position]].name == name {
|
||||
stack = stack[:position]
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
sectionIndex := len(sections)
|
||||
sections = append(sections, metadataSection{name: name, kind: strings.ToLower(strings.TrimSpace(name)), depth: depth})
|
||||
if pairedNames[name] {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
section := §ions[sectionIndex]
|
||||
isString := typ == 3 || typ == 5 || typ == 6 || typ == 7 || typ == 8 || typ == 10
|
||||
switch section.kind {
|
||||
case "name":
|
||||
if section.depth != 0 || len(section.values) != 0 || !isString {
|
||||
continue
|
||||
}
|
||||
case "growtype name":
|
||||
if section.depth != 0 || len(section.values) != 0 {
|
||||
continue
|
||||
}
|
||||
case "rarity":
|
||||
if len(section.values) != 0 {
|
||||
continue
|
||||
}
|
||||
case "icon", "field image":
|
||||
if len(section.values) == 2 {
|
||||
continue // the first valid adjacent path/index pair is complete
|
||||
}
|
||||
default:
|
||||
continue // no decoding or value allocation for unrelated sections
|
||||
}
|
||||
text := ""
|
||||
if isString {
|
||||
text = a.ResolveString(value)
|
||||
}
|
||||
token := metadataToken{typ: typ, value: value, text: text}
|
||||
switch section.kind {
|
||||
case "name":
|
||||
if strings.TrimSpace(text) == "" {
|
||||
continue
|
||||
}
|
||||
case "icon", "field image":
|
||||
if len(section.values) == 1 && strings.TrimSpace(section.values[0].text) != "" && typ == 0 && value >= 0 {
|
||||
section.values = append(section.values, token)
|
||||
continue
|
||||
}
|
||||
// Only the previous token can form a pair with the next one.
|
||||
if len(section.values) != 0 {
|
||||
section.values[0] = token
|
||||
continue
|
||||
}
|
||||
}
|
||||
section.values = append(section.values, token)
|
||||
}
|
||||
|
||||
metadata := ScriptMetadata{}
|
||||
for _, section := range sections {
|
||||
name := section.kind
|
||||
switch name {
|
||||
case "name":
|
||||
// Entity names only come from top-level sections; a nested [name]
|
||||
// belongs to an embedded sub-record, not to the file itself.
|
||||
if section.depth != 0 {
|
||||
continue
|
||||
}
|
||||
if metadata.HasName {
|
||||
continue
|
||||
}
|
||||
for _, token := range section.values {
|
||||
if (token.typ == 5 || token.typ == 6 || token.typ == 7 || token.typ == 8 || token.typ == 10 || token.typ == 3) && strings.TrimSpace(token.text) != "" {
|
||||
metadata.Name = token.text
|
||||
metadata.HasName = true
|
||||
break
|
||||
}
|
||||
}
|
||||
case "icon", "field image":
|
||||
if len(section.values) < 2 {
|
||||
continue
|
||||
}
|
||||
for index := 0; index+1 < len(section.values); index++ {
|
||||
pathToken := section.values[index]
|
||||
imageIndexToken := section.values[index+1]
|
||||
if (pathToken.typ != 3 && pathToken.typ != 5 && pathToken.typ != 6 && pathToken.typ != 7 && pathToken.typ != 8 && pathToken.typ != 10) || strings.TrimSpace(pathToken.text) == "" || imageIndexToken.typ != 0 || imageIndexToken.value < 0 {
|
||||
continue
|
||||
}
|
||||
reference := &ScriptImageReference{Path: strings.TrimSpace(pathToken.text), Index: imageIndexToken.value}
|
||||
if name == "icon" && metadata.Icon == nil {
|
||||
metadata.Icon = reference
|
||||
}
|
||||
if name == "field image" && metadata.FieldImage == nil {
|
||||
metadata.FieldImage = reference
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rarity":
|
||||
// The preview reads the same field as "the first [rarity] token in
|
||||
// document order", so the index uses that occurrence too. Both
|
||||
// equipment and stackables store the value as an integer token, but
|
||||
// a quoted number is also accepted.
|
||||
if metadata.HasRarity || len(section.values) == 0 {
|
||||
continue
|
||||
}
|
||||
value := section.values[0]
|
||||
if value.typ == 0 {
|
||||
metadata.Rarity, metadata.HasRarity = value.value, true
|
||||
continue
|
||||
}
|
||||
if parsed, err := strconv.ParseInt(strings.TrimSpace(value.text), 10, 32); err == nil {
|
||||
metadata.Rarity, metadata.HasRarity = int32(parsed), true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Character lists identify base jobs, not the generic [name] of a character.
|
||||
// Keep this in the shared metadata projection so every index and relation
|
||||
// uses the same name and localization dependency tracking.
|
||||
path := strings.ToLower(a.Path(i))
|
||||
if strings.HasPrefix(path, "character/") && strings.HasSuffix(path, ".chr") {
|
||||
for _, section := range sections {
|
||||
if section.depth != 0 || !strings.EqualFold(section.name, "growtype name") || len(section.values) == 0 {
|
||||
continue
|
||||
}
|
||||
if value := section.values[0].text; strings.TrimSpace(value) != "" {
|
||||
metadata.Name, metadata.HasName = value, true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if metadata.HasName {
|
||||
metadata.StringTableReferences = stringTableReferences(metadata.Name)
|
||||
metadata.Name, metadata.NameFallback = a.resolvePlaceholders(metadata.Name, "")
|
||||
}
|
||||
if metadata.Icon != nil {
|
||||
metadata.StringTableReferences = append(metadata.StringTableReferences, stringTableReferences(metadata.Icon.Path)...)
|
||||
}
|
||||
if metadata.FieldImage != nil {
|
||||
metadata.StringTableReferences = append(metadata.StringTableReferences, stringTableReferences(metadata.FieldImage.Path)...)
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func stringTableReferences(text string) []StringTableReference {
|
||||
if !strings.Contains(text, "<") || !strings.Contains(text, "::") {
|
||||
return nil
|
||||
}
|
||||
result := make([]StringTableReference, 0, 1)
|
||||
seen := make(map[string]struct{})
|
||||
for offset := 0; offset < len(text); {
|
||||
start := strings.IndexByte(text[offset:], '<')
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
start += offset
|
||||
end := strings.IndexByte(text[start:], '>')
|
||||
if end < 0 {
|
||||
break
|
||||
}
|
||||
end += start
|
||||
if index, key, ok := parsePlaceholder(text[start : end+1]); ok {
|
||||
identity := fmt.Sprintf("%d\x00%s", index, key)
|
||||
if _, exists := seen[identity]; !exists {
|
||||
seen[identity] = struct{}{}
|
||||
result = append(result, StringTableReference{Index: index, Key: key})
|
||||
}
|
||||
}
|
||||
offset = end + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ScriptName extracts the first direct string value from the [name] section
|
||||
// without formatting or materializing the full decompiled script.
|
||||
func (a *Archive) ScriptName(i int32) (string, bool, error) {
|
||||
metadata, err := a.ScriptMetadata(i)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return metadata.Name, metadata.HasName, nil
|
||||
}
|
||||
|
||||
type scriptMetadataValue struct {
|
||||
text string
|
||||
}
|
||||
|
||||
func (a *Archive) scriptMetadataValue(typ byte, value int32) (scriptMetadataValue, bool) {
|
||||
switch typ {
|
||||
case 0:
|
||||
return scriptMetadataValue{text: strconv.FormatInt(int64(value), 10)}, true
|
||||
case 2:
|
||||
return scriptMetadataValue{text: strconv.FormatFloat(float64(math.Float32frombits(uint32(value))), 'g', -1, 32)}, true
|
||||
case 3, 5, 6, 7, 8, 10:
|
||||
return scriptMetadataValue{text: a.ResolveString(value)}, true
|
||||
default:
|
||||
return scriptMetadataValue{}, false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user