Files
90CNServer/pvf/internal/pvf/text_encoding.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

184 lines
4.5 KiB
Go

package pvf
import (
"bytes"
"encoding/binary"
"fmt"
"unicode/utf16"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/korean"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese"
textunicode "golang.org/x/text/encoding/unicode"
)
// TextEncoding is the session override; auto follows the container convention.
func (a *Archive) TextEncoding() string {
if a.textEncoding == "" {
return "auto"
}
return a.textEncoding
}
func (a *Archive) TextEncodingLabel() string {
if a.textEncoding != "" {
if a.act60 == nil && a.tw == nil && a.textEncoding != "UTF-16LE" {
if !a.hasUTF8PoolLocked() {
return "UTF-16LE"
}
return a.textEncoding + " / UTF-16LE"
}
return a.textEncoding
}
switch a.Format() {
case FormatACT60:
return "GBK"
case FormatTW:
return "Big5"
default:
if !a.hasUTF8PoolLocked() {
return "UTF-16LE"
}
return "UTF-8 / UTF-16LE"
}
}
func textCodec(name string) (encoding.Encoding, error) {
switch name {
case "", "auto", "UTF-8":
return nil, nil
case "GBK":
return simplifiedchinese.GBK, nil
case "Big5":
return traditionalchinese.Big5, nil
case "CP949":
return korean.EUCKR, nil
case "UTF-16LE":
return textunicode.UTF16(textunicode.LittleEndian, textunicode.IgnoreBOM), nil
default:
return nil, fmt.Errorf("不支持的文本编码:%s", name)
}
}
func (a *Archive) nativeTextCodec() encoding.Encoding {
name := a.textEncoding
if name == "" {
if a.tw != nil {
name = "Big5"
} else {
name = "GBK"
}
}
codec, _ := textCodec(name)
return codec
}
func (a *Archive) decodeNativeText(raw []byte) string {
// A BOM identifies a file-local UTF-8 exception regardless of the default.
if bytes.HasPrefix(raw, []byte{0xef, 0xbb, 0xbf}) {
return string(raw[3:])
}
if a.textEncoding == "UTF-16LE" {
raw = bytes.TrimPrefix(raw, []byte{0xff, 0xfe})
}
if codec := a.nativeTextCodec(); codec != nil {
decoded, _ := codec.NewDecoder().Bytes(raw)
return string(decoded)
}
return string(raw)
}
func (a *Archive) encodeNativeText(text string) ([]byte, error) {
if codec := a.nativeTextCodec(); codec != nil {
return codec.NewEncoder().Bytes([]byte(text))
}
return []byte(text), nil
}
func (a *Archive) decodeNarrowText(raw []byte) string {
// Narrow pools have byte-aligned NUL terminators; their layout cannot
// contain UTF-16 code units. Unicode entries already use the wide pool.
if a.textEncoding == "UTF-16LE" {
return string(raw)
}
if codec, _ := textCodec(a.textEncoding); codec != nil {
decoded, _ := codec.NewDecoder().Bytes(raw)
return string(decoded)
}
return string(raw)
}
func (a *Archive) nativePayloadIsText(raw []byte) bool {
if a.textEncoding != "UTF-16LE" || bytes.HasPrefix(raw, []byte{0xef, 0xbb, 0xbf}) {
return !bytes.ContainsRune(raw, 0)
}
raw = bytes.TrimPrefix(raw, []byte{0xff, 0xfe})
if len(raw)%2 != 0 {
return false
}
for i := 0; i < len(raw); i += 2 {
u := binary.LittleEndian.Uint16(raw[i:])
if u < 0x20 && u != '\t' && u != '\r' && u != '\n' {
return false
}
if utf16.IsSurrogate(rune(u)) {
if u < 0xd800 || u > 0xdbff || i+3 >= len(raw) {
return false
}
low := binary.LittleEndian.Uint16(raw[i+2:])
if low < 0xdc00 || low > 0xdfff {
return false
}
i += 2
}
}
return true
}
// WithTextEncoding builds an isolated interpretation of the current source.
// Pending edits must be saved first; switching never transcodes untouched bytes.
func (a *Archive) WithTextEncoding(name string) (*Archive, error) {
if _, err := textCodec(name); err != nil {
return nil, err
}
if a.Modified() {
return nil, fmt.Errorf("请先保存归档修改,再切换编码")
}
if name == "auto" {
name = ""
}
var next *Archive
var err error
switch {
case a.act60 != nil:
tree, body, count, legacy, ok := probeACT60(a.data)
if !ok {
return nil, ErrInvalidSection
}
next, err = parseACT60(a.data, tree, body, count, legacy, name)
case a.tw != nil:
next, err = parseTW(a.data, name)
default:
next = a.CloneForBatch()
next.textEncoding = name
next.resolveCache = make(map[int32]string)
next.resolveCacheOrder = nil
next.resolveCacheBytes = 0
next.strAIdx, next.strWIdx = nil, nil
next.rebuildPathIndex()
}
if err != nil {
return nil, err
}
next.textEncoding = name
next.sourcePath = a.sourcePath
next.scriptRenderer = a.scriptRenderer
next.canonicalScriptRenderer = a.canonicalScriptRenderer
for i := range next.items {
next.items[i].identity = a.items[i].identity
}
next.ClearMutations()
return next, nil
}