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,188 @@
|
||||
// Command pvfcopyraw copies an explicit allowlist of existing raw members
|
||||
// from a donor PVF into a base PVF. It never edits either input in place.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
basePath := flag.String("base", "", "base PVF")
|
||||
donorPath := flag.String("donor", "", "donor PVF")
|
||||
outputPath := flag.String("out", "", "output PVF")
|
||||
files := flag.String("files", "", "comma-separated existing archive members")
|
||||
remapType1 := flag.Bool("remap-type1", false, "remap type-1 string tokens by value into the base PVF string pool")
|
||||
flag.Parse()
|
||||
|
||||
if strings.TrimSpace(*basePath) == "" || strings.TrimSpace(*donorPath) == "" ||
|
||||
strings.TrimSpace(*outputPath) == "" || strings.TrimSpace(*files) == "" {
|
||||
panic("-base, -donor, -out and -files are required")
|
||||
}
|
||||
baseAbsolute := mustAbsolute(*basePath)
|
||||
donorAbsolute := mustAbsolute(*donorPath)
|
||||
outputAbsolute := mustAbsolute(*outputPath)
|
||||
if strings.EqualFold(baseAbsolute, outputAbsolute) || strings.EqualFold(donorAbsolute, outputAbsolute) {
|
||||
panic("output must differ from both inputs")
|
||||
}
|
||||
|
||||
base, err := platformpvf.Open(baseAbsolute)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
donor, err := platformpvf.Open(donorAbsolute)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
paths := parsePaths(*files)
|
||||
type pendingReplacement struct {
|
||||
path string
|
||||
raw []byte
|
||||
}
|
||||
pending := make([]pendingReplacement, 0, len(paths))
|
||||
wantedStrings := make([]string, 0)
|
||||
wantedWide := make(map[string]bool)
|
||||
for _, memberPath := range paths {
|
||||
if _, found := base.FindFile(memberPath); !found {
|
||||
panic(fmt.Sprintf("base member does not exist: %s", memberPath))
|
||||
}
|
||||
donorRaw, readErr := donor.ReadRaw(memberPath)
|
||||
if readErr != nil {
|
||||
panic(readErr)
|
||||
}
|
||||
baseRaw, readErr := base.ReadRaw(memberPath)
|
||||
if readErr != nil {
|
||||
panic(readErr)
|
||||
}
|
||||
if bytes.Equal(baseRaw, donorRaw) {
|
||||
panic(fmt.Sprintf("member is already identical: %s", memberPath))
|
||||
}
|
||||
pending = append(pending, pendingReplacement{path: memberPath, raw: donorRaw})
|
||||
if *remapType1 {
|
||||
if len(donorRaw)%5 != 0 {
|
||||
panic(fmt.Sprintf("type-1 member length is not token-aligned: %s bytes=%d", memberPath, len(donorRaw)))
|
||||
}
|
||||
for offset := 0; offset < len(donorRaw); offset += 5 {
|
||||
if donorRaw[offset] != 3 && donorRaw[offset] != 6 {
|
||||
continue
|
||||
}
|
||||
magic := int(int32(binary.LittleEndian.Uint32(donorRaw[offset+1 : offset+5])))
|
||||
value := donor.ResolveString(magic)
|
||||
wantedStrings = append(wantedStrings, value)
|
||||
wantedWide[value] = wantedWide[value] || magic&1 != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
stringOffsets := base.FindStringMagicOffsets(wantedStrings)
|
||||
if *remapType1 && len(stringOffsets) < len(wantedWide) {
|
||||
appends := make([]platformpvf.StringAppend, 0, len(wantedWide)-len(stringOffsets))
|
||||
for value, wide := range wantedWide {
|
||||
if _, found := stringOffsets[value]; found || value == "" {
|
||||
continue
|
||||
}
|
||||
appends = append(appends, platformpvf.StringAppend{Value: value, Wide: wide})
|
||||
}
|
||||
if len(appends) > 0 {
|
||||
withStrings, appendErr := base.RepackAppendStrings(appends)
|
||||
if appendErr != nil {
|
||||
panic(appendErr)
|
||||
}
|
||||
base, err = platformpvf.OpenBytes(withStrings)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stringOffsets = base.FindStringMagicOffsets(wantedStrings)
|
||||
}
|
||||
}
|
||||
replacements := make([]platformpvf.RawReplacement, 0, len(pending))
|
||||
for _, replacement := range pending {
|
||||
raw := append([]byte(nil), replacement.raw...)
|
||||
if *remapType1 {
|
||||
for offset := 0; offset < len(raw); offset += 5 {
|
||||
if raw[offset] != 3 && raw[offset] != 6 {
|
||||
continue
|
||||
}
|
||||
donorMagic := int(int32(binary.LittleEndian.Uint32(raw[offset+1 : offset+5])))
|
||||
value := donor.ResolveString(donorMagic)
|
||||
baseMagic, found := stringOffsets[value]
|
||||
if !found {
|
||||
panic(fmt.Sprintf("base string pool is missing %q (donor_magic=%d wide=%t) required by %s", value, donorMagic, donorMagic&1 != 0, replacement.path))
|
||||
}
|
||||
binary.LittleEndian.PutUint32(raw[offset+1:offset+5], uint32(int32(baseMagic)))
|
||||
}
|
||||
}
|
||||
replacements = append(replacements, platformpvf.RawReplacement{Path: replacement.path, Data: raw})
|
||||
}
|
||||
|
||||
output, err := base.RepackRaw(replacements)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
verified, err := platformpvf.OpenBytes(output)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, replacement := range replacements {
|
||||
got, readErr := verified.ReadRaw(replacement.Path)
|
||||
if readErr != nil {
|
||||
panic(readErr)
|
||||
}
|
||||
if !bytes.Equal(got, replacement.Data) {
|
||||
panic(fmt.Sprintf("verification mismatch: %s", replacement.Path))
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(outputAbsolute), 0o755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := os.Stat(outputAbsolute); err == nil {
|
||||
panic(fmt.Sprintf("output already exists: %s", outputAbsolute))
|
||||
} else if !os.IsNotExist(err) {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.WriteFile(outputAbsolute, output, 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
hash := sha256.Sum256(output)
|
||||
fmt.Printf("copied=%d size=%d sha256=%X output=%s\n", len(replacements), len(output), hash, outputAbsolute)
|
||||
for _, replacement := range replacements {
|
||||
fmt.Printf("COPIED %s bytes=%d\n", replacement.Path, len(replacement.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func mustAbsolute(value string) string {
|
||||
absolute, err := filepath.Abs(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return absolute
|
||||
}
|
||||
|
||||
func parsePaths(value string) []string {
|
||||
paths := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, candidate := range strings.Split(value, ",") {
|
||||
candidate = strings.TrimSpace(strings.ReplaceAll(candidate, "\\", "/"))
|
||||
candidate = strings.TrimPrefix(candidate, "/")
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(candidate)
|
||||
if _, duplicate := seen[key]; duplicate {
|
||||
panic(fmt.Sprintf("duplicate member: %s", candidate))
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
paths = append(paths, candidate)
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
panic("no archive members supplied")
|
||||
}
|
||||
return paths
|
||||
}
|
||||
Reference in new issue
Block a user