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,255 @@
|
||||
// pvfgoldkaleidoearring extends legacy golden-kaleido special-equipment
|
||||
// filters with the earring type introduced after those items were authored.
|
||||
// The input archive is never modified in place.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const stackableListPath = "stackable/stackable.lst"
|
||||
|
||||
type goldKaleidoEntry struct {
|
||||
itemID int64
|
||||
path string
|
||||
name string
|
||||
equipTypes []string
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "pvfgoldkaleidoearring:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
input := flag.String("pvf", "", "input PVF path (never modified)")
|
||||
output := flag.String("out", "", "output PVF path (must differ from input)")
|
||||
flag.Parse()
|
||||
if strings.TrimSpace(*input) == "" || strings.TrimSpace(*output) == "" {
|
||||
return fmt.Errorf("-pvf and -out are required")
|
||||
}
|
||||
inputAbs, err := filepath.Abs(*input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputAbs, err := filepath.Abs(*output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.EqualFold(inputAbs, outputAbs) {
|
||||
return fmt.Errorf("output must differ from input")
|
||||
}
|
||||
inputBytes, err := os.ReadFile(inputAbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
archive, err := platformpvf.OpenBytes(inputBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := loadGoldKaleidoEntries(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var targets []goldKaleidoEntry
|
||||
for _, entry := range entries {
|
||||
if shouldAddEarring(entry.equipTypes) {
|
||||
targets = append(targets, entry)
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return fmt.Errorf("no legacy golden-kaleido special-equipment filter requires an earring patch")
|
||||
}
|
||||
earringMagic, err := findEarringMagic(archive, entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
replacements := make([]platformpvf.RawReplacement, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
raw, readErr := archive.ReadRaw(target.path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
patched, patchErr := appendSectionStringToken(archive, raw, "[usable equip type]", "[/usable equip type]", "[earring]", earringMagic)
|
||||
if patchErr != nil {
|
||||
return fmt.Errorf("item=%d path=%s: %w", target.itemID, target.path, patchErr)
|
||||
}
|
||||
replacements = append(replacements, platformpvf.RawReplacement{Path: target.path, Data: patched})
|
||||
}
|
||||
outputBytes, err := archive.RepackRaw(replacements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verified, err := platformpvf.OpenBytes(outputBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reopen output: %w", err)
|
||||
}
|
||||
verifiedEntries, err := loadGoldKaleidoEntries(verified)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range verifiedEntries {
|
||||
if hasType(entry.equipTypes, "[support]") || hasType(entry.equipTypes, "[magic stone]") {
|
||||
if !hasType(entry.equipTypes, "[earring]") {
|
||||
return fmt.Errorf("verification item=%d path=%s still omits [earring]", entry.itemID, entry.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(outputAbs), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(outputAbs, outputBytes, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("input_sha256=%X\noutput_sha256=%X\npatched=%d output=%s\n", sha256.Sum256(inputBytes), sha256.Sum256(outputBytes), len(targets), outputAbs)
|
||||
for _, target := range targets {
|
||||
fmt.Printf("item=%d name=%q path=%s added=[earring]\n", target.itemID, target.name, target.path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadGoldKaleidoEntries(archive *platformpvf.Archive) ([]goldKaleidoEntry, error) {
|
||||
listText, err := archive.ReadText(stackableListPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listDocument, err := dnfpvf.Parse(stackableListPath, listText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]goldKaleidoEntry, 0, 32)
|
||||
for _, listed := range dnfpvf.ParseList(listDocument) {
|
||||
docPath := path.Clean(path.Join("stackable", strings.TrimPrefix(strings.ReplaceAll(strings.TrimSpace(listed.Path), "\\", "/"), "/")))
|
||||
text, readErr := archive.ReadText(docPath)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
document, parseErr := dnfpvf.Parse(docPath, text)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
stackableType, _ := document.Text("stackable type")
|
||||
actionType, _ := document.Text("action type")
|
||||
if !isGoldKaleido(stackableType, actionType) {
|
||||
continue
|
||||
}
|
||||
name, _ := document.Text("name")
|
||||
entries = append(entries, goldKaleidoEntry{
|
||||
itemID: listed.ID,
|
||||
path: docPath,
|
||||
name: strings.TrimSpace(name),
|
||||
equipTypes: normalizedTypes(document.Texts("usable equip type")),
|
||||
})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].itemID < entries[j].itemID })
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func isGoldKaleido(stackableType, actionType string) bool {
|
||||
return normalizedType(stackableType) == "[gold kaleido]" || normalizedType(actionType) == "[gold kaleido box]"
|
||||
}
|
||||
|
||||
func shouldAddEarring(types []string) bool {
|
||||
return (hasType(types, "[support]") || hasType(types, "[magic stone]")) && !hasType(types, "[earring]")
|
||||
}
|
||||
|
||||
func normalizedTypes(values []string) []string {
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = normalizedType(value); value != "" {
|
||||
out = append(out, value)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizedType(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(strings.ReplaceAll(value, "`", "")))
|
||||
}
|
||||
|
||||
func hasType(types []string, wanted string) bool {
|
||||
wanted = normalizedType(wanted)
|
||||
for _, value := range types {
|
||||
if normalizedType(value) == wanted {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findEarringMagic(archive *platformpvf.Archive, entries []goldKaleidoEntry) (int32, error) {
|
||||
for _, entry := range entries {
|
||||
if !hasType(entry.equipTypes, "[earring]") {
|
||||
continue
|
||||
}
|
||||
raw, err := archive.ReadRaw(entry.path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for offset := 0; offset+5 <= len(raw); offset += 5 {
|
||||
if raw[offset] != 6 {
|
||||
continue
|
||||
}
|
||||
magic := int32(binary.LittleEndian.Uint32(raw[offset+1 : offset+5]))
|
||||
if normalizedType(archive.ResolveString(int(magic))) == "[earring]" {
|
||||
return magic, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("shared string token for [earring] was not found in a proven golden-kaleido definition")
|
||||
}
|
||||
|
||||
func appendSectionStringToken(archive *platformpvf.Archive, raw []byte, open, close, value string, magic int32) ([]byte, error) {
|
||||
if len(raw)%5 != 0 {
|
||||
return nil, fmt.Errorf("raw token bytes=%d are not aligned", len(raw))
|
||||
}
|
||||
inside := false
|
||||
closeOffset := -1
|
||||
for offset := 0; offset+5 <= len(raw); offset += 5 {
|
||||
kind := raw[offset]
|
||||
tokenMagic := int32(binary.LittleEndian.Uint32(raw[offset+1 : offset+5]))
|
||||
if kind == 3 {
|
||||
resolved := archive.ResolveString(int(tokenMagic))
|
||||
switch resolved {
|
||||
case open:
|
||||
if inside || closeOffset >= 0 {
|
||||
return nil, fmt.Errorf("duplicate section %s", open)
|
||||
}
|
||||
inside = true
|
||||
case close:
|
||||
if inside {
|
||||
closeOffset = offset
|
||||
inside = false
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if inside && kind == 6 && normalizedType(archive.ResolveString(int(tokenMagic))) == normalizedType(value) {
|
||||
return nil, fmt.Errorf("section already contains %s", value)
|
||||
}
|
||||
}
|
||||
if closeOffset < 0 {
|
||||
return nil, fmt.Errorf("section %s...%s not found", open, close)
|
||||
}
|
||||
var token [5]byte
|
||||
token[0] = 6
|
||||
binary.LittleEndian.PutUint32(token[1:], uint32(magic))
|
||||
patched := make([]byte, 0, len(raw)+len(token))
|
||||
patched = append(patched, raw[:closeOffset]...)
|
||||
patched = append(patched, token[:]...)
|
||||
patched = append(patched, raw[closeOffset:]...)
|
||||
return patched, nil
|
||||
}
|
||||
Reference in new issue
Block a user