Files
90CNServer/server/cmd/scratch/pvfaria/main.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

352 lines
12 KiB
Go

// Command pvfaria removes the duplicate Aelia NPC 20462 from Hendon Myre
// while preserving the existing Aelia NPC 20449 and every unrelated PVF file.
// It always writes a separate candidate and never edits the active PVF.
package main
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
npcListPath = "npc/npc.lst"
itemShopListPath = "itemshop/itemshop.lst"
sourceShopPath = "itemshop/chn_masang_shop_2017.shp"
targetNPCPath = "npc/chn_event_lions10th_2.npc"
townMapPath = "map/cataclysm/town/hendonmyre/new_hendon_main.map"
)
type report struct {
InputPath string `json:"input_path"`
OutputPath string `json:"output_path"`
RemovedNPCID int `json:"removed_npc_id"`
PreservedNPCID int `json:"preserved_npc_id"`
PreservedX int `json:"preserved_x"`
PreservedY int `json:"preserved_y"`
RemovedShopID int `json:"removed_shop_id"`
SourceShopOwner int `json:"source_shop_owner"`
FileCount int `json:"file_count"`
InputSize int64 `json:"input_size"`
OutputSize int `json:"output_size"`
OutputSHA256 string `json:"output_sha256"`
}
func main() {
pvfPath := flag.String("pvf", `D:/DNF/runtime/data/dnf/Script.pvf`, "source Script.pvf")
outPath := flag.String("out", `D:/DNF/runtime/tmp/aria-npc-remove/Script-no-duplicate-aelia.pvf`, "candidate output path")
flag.Parse()
inputAbs, err := filepath.Abs(*pvfPath)
must(err)
outputAbs, err := filepath.Abs(*outPath)
must(err)
if strings.EqualFold(inputAbs, outputAbs) {
panic("output must differ from input")
}
archive, err := platformpvf.Open(inputAbs)
must(err)
fileCount := len(archive.Files())
preservedTargetNPC, err := archive.ReadRaw(targetNPCPath)
must(err)
npcListRaw, err := archive.ReadRaw(npcListPath)
must(err)
npcListPatched, err := removeListEntry(archive, npcListRaw, 20462, "chn_masang_2017.npc")
must(err)
itemShopListRaw, err := archive.ReadRaw(itemShopListPath)
must(err)
itemShopListPatched, err := removeListEntry(archive, itemShopListRaw, 101262, "chn_masang_shop_2017.shp")
must(err)
sourceShopRaw, err := archive.ReadRaw(sourceShopPath)
must(err)
sourceShopPatched, err := replaceSectionIntValue(archive, sourceShopRaw, "npc", 20462, 20325)
must(err)
townMapRaw, err := archive.ReadRaw(townMapPath)
must(err)
townMapPatched, err := removeTownNPCPlacement(archive, townMapRaw, 20462, "left", 2088, 156, 0)
must(err)
patched, err := archive.RepackRaw([]platformpvf.RawReplacement{
{Path: npcListPath, Data: npcListPatched},
{Path: itemShopListPath, Data: itemShopListPatched},
{Path: sourceShopPath, Data: sourceShopPatched},
{Path: townMapPath, Data: townMapPatched},
})
must(err)
verified, err := platformpvf.OpenBytes(patched)
must(err)
if len(verified.Files()) != fileCount {
panic(fmt.Errorf("file count changed: got=%d want=%d", len(verified.Files()), fileCount))
}
must(requireListEntryAbsent(verified, npcListPath, 20462))
must(requireListEntry(verified, npcListPath, 20449, "chn_event_lions10th_2.npc"))
must(requireListEntryAbsent(verified, itemShopListPath, 101262))
must(requireListEntry(verified, itemShopListPath, 400000022, "chn_10th_lions_2.shp"))
must(requireSectionIntValue(verified, sourceShopPath, "npc", 20325))
must(requireTownNPCPlacementAbsent(verified, townMapPath, 20462))
must(requireTownNPCPlacement(verified, townMapPath, 20449, "left", 1478, 354, 0))
must(requireTownNPCPlacement(verified, townMapPath, 159, "left", 1896, 156, 0))
must(requireTownNPCPlacement(verified, townMapPath, 45, "right", 2470, 156, 0))
verifiedTargetNPC, err := verified.ReadRaw(targetNPCPath)
must(err)
if string(verifiedTargetNPC) != string(preservedTargetNPC) {
panic(fmt.Errorf("preserved Aelia NPC %d content changed", 20449))
}
verifiedTargetText, err := verified.ReadText(targetNPCPath)
must(err)
for _, required := range []string{"[name]\n`艾莉亚`", "`[joust event]` 0", "`[item shop]` 400000022"} {
if !strings.Contains(verifiedTargetText, required) {
panic(fmt.Errorf("preserved Aelia NPC is missing %q", required))
}
}
must(os.MkdirAll(filepath.Dir(outputAbs), 0o755))
must(os.WriteFile(outputAbs, patched, 0o644))
inputInfo, err := os.Stat(inputAbs)
must(err)
sum := sha256.Sum256(patched)
result := report{
InputPath: inputAbs,
OutputPath: outputAbs,
RemovedNPCID: 20462,
PreservedNPCID: 20449,
PreservedX: 1478,
PreservedY: 354,
RemovedShopID: 101262,
SourceShopOwner: 20325,
FileCount: fileCount,
InputSize: inputInfo.Size(),
OutputSize: len(patched),
OutputSHA256: strings.ToUpper(hex.EncodeToString(sum[:])),
}
encoded, err := json.MarshalIndent(result, "", " ")
must(err)
fmt.Println(string(encoded))
}
func removeListEntry(archive *platformpvf.Archive, raw []byte, id int, wantPath string) ([]byte, error) {
if archive == nil || len(raw)%5 != 0 {
return nil, fmt.Errorf("list token stream is invalid: bytes=%d", len(raw))
}
out := make([]byte, 0, len(raw)-10)
matches := 0
for offset := 0; offset < len(raw); {
if offset+10 <= len(raw) && raw[offset] == 0 && scriptTokenInt(raw[offset:offset+5]) == id {
pathToken := raw[offset+5 : offset+10]
if pathToken[0] != 6 {
return nil, fmt.Errorf("list id %d is not followed by a string path", id)
}
gotPath := archive.ResolveString(scriptTokenInt(pathToken))
if !strings.EqualFold(gotPath, wantPath) {
return nil, fmt.Errorf("list id %d path=%q, want=%q", id, gotPath, wantPath)
}
matches++
offset += 10
continue
}
out = append(out, raw[offset:offset+5]...)
offset += 5
}
if matches != 1 {
return nil, fmt.Errorf("list id %d matches=%d, want=1", id, matches)
}
return out, nil
}
func removeTownNPCPlacement(archive *platformpvf.Archive, raw []byte, npcID int, direction string, x, y, z int) ([]byte, error) {
if archive == nil || len(raw)%5 != 0 {
return nil, fmt.Errorf("map token stream is invalid: bytes=%d", len(raw))
}
out := make([]byte, 0, len(raw)-25)
inNPCSection := false
matches := 0
for offset := 0; offset < len(raw); {
token := raw[offset : offset+5]
if token[0] == 3 {
inNPCSection = normalizedLabel(archive.ResolveString(scriptTokenInt(token))) == "npc"
}
if inNPCSection && offset+25 <= len(raw) && token[0] == 0 && scriptTokenInt(token) == npcID {
directionToken := raw[offset+5 : offset+10]
xToken := raw[offset+10 : offset+15]
yToken := raw[offset+15 : offset+20]
zToken := raw[offset+20 : offset+25]
if directionToken[0] != 6 || normalizedLabel(archive.ResolveString(scriptTokenInt(directionToken))) != normalizedLabel(direction) ||
xToken[0] != 0 || scriptTokenInt(xToken) != x || yToken[0] != 0 || scriptTokenInt(yToken) != y ||
zToken[0] != 0 || scriptTokenInt(zToken) != z {
return nil, fmt.Errorf("town NPC %d tuple differs from the expected duplicate placement", npcID)
}
matches++
offset += 25
continue
}
out = append(out, token...)
offset += 5
}
if matches != 1 {
return nil, fmt.Errorf("town NPC %d placement matches=%d, want=1", npcID, matches)
}
return out, nil
}
func replaceSectionIntValue(archive *platformpvf.Archive, raw []byte, section string, oldValue, newValue int) ([]byte, error) {
if archive == nil || len(raw)%5 != 0 {
return nil, fmt.Errorf("script token stream is invalid: bytes=%d", len(raw))
}
out := append([]byte(nil), raw...)
matches := 0
for offset := 0; offset+10 <= len(out); offset += 5 {
token := out[offset : offset+5]
if (token[0] != 3 && token[0] != 6) || normalizedLabel(archive.ResolveString(scriptTokenInt(token))) != normalizedLabel(section) {
continue
}
valueToken := out[offset+5 : offset+10]
if valueToken[0] != 0 || scriptTokenInt(valueToken) != oldValue {
return nil, fmt.Errorf("section [%s] value does not match %d", section, oldValue)
}
binary.LittleEndian.PutUint32(valueToken[1:5], uint32(int32(newValue)))
matches++
}
if matches != 1 {
return nil, fmt.Errorf("section [%s] matches=%d, want=1", section, matches)
}
return out, nil
}
func requireListEntry(archive *platformpvf.Archive, path string, id int, wantPath string) error {
raw, err := archive.ReadRaw(path)
if err != nil {
return err
}
matches := 0
for offset := 0; offset+10 <= len(raw); offset += 5 {
if raw[offset] != 0 || scriptTokenInt(raw[offset:offset+5]) != id {
continue
}
pathToken := raw[offset+5 : offset+10]
if pathToken[0] != 6 || !strings.EqualFold(archive.ResolveString(scriptTokenInt(pathToken)), wantPath) {
return fmt.Errorf("list %s id %d does not map to %q", path, id, wantPath)
}
matches++
}
if matches != 1 {
return fmt.Errorf("list %s id %d matches=%d, want=1", path, id, matches)
}
return nil
}
func requireListEntryAbsent(archive *platformpvf.Archive, path string, id int) error {
raw, err := archive.ReadRaw(path)
if err != nil {
return err
}
for offset := 0; offset+5 <= len(raw); offset += 5 {
if raw[offset] == 0 && scriptTokenInt(raw[offset:offset+5]) == id {
return fmt.Errorf("list %s still contains id %d", path, id)
}
}
return nil
}
func requireSectionIntValue(archive *platformpvf.Archive, path, section string, want int) error {
raw, err := archive.ReadRaw(path)
if err != nil {
return err
}
matches := 0
for offset := 0; offset+10 <= len(raw); offset += 5 {
token := raw[offset : offset+5]
if (token[0] != 3 && token[0] != 6) || normalizedLabel(archive.ResolveString(scriptTokenInt(token))) != normalizedLabel(section) {
continue
}
valueToken := raw[offset+5 : offset+10]
if valueToken[0] != 0 || scriptTokenInt(valueToken) != want {
return fmt.Errorf("section [%s] does not contain required integer %d", section, want)
}
matches++
}
if matches != 1 {
return fmt.Errorf("section [%s] matches=%d, want=1", section, matches)
}
return nil
}
func requireTownNPCPlacement(archive *platformpvf.Archive, path string, npcID int, direction string, x, y, z int) error {
raw, err := archive.ReadRaw(path)
if err != nil {
return err
}
inNPCSection := false
matches := 0
for offset := 0; offset+25 <= len(raw); offset += 5 {
token := raw[offset : offset+5]
if token[0] == 3 {
inNPCSection = normalizedLabel(archive.ResolveString(scriptTokenInt(token))) == "npc"
continue
}
if !inNPCSection || token[0] != 0 || scriptTokenInt(token) != npcID {
continue
}
directionToken := raw[offset+5 : offset+10]
xToken := raw[offset+10 : offset+15]
yToken := raw[offset+15 : offset+20]
zToken := raw[offset+20 : offset+25]
if directionToken[0] != 6 || normalizedLabel(archive.ResolveString(scriptTokenInt(directionToken))) != normalizedLabel(direction) ||
xToken[0] != 0 || scriptTokenInt(xToken) != x || yToken[0] != 0 || scriptTokenInt(yToken) != y ||
zToken[0] != 0 || scriptTokenInt(zToken) != z {
return fmt.Errorf("town NPC %d placement differs from required tuple", npcID)
}
matches++
}
if matches != 1 {
return fmt.Errorf("town NPC %d placement matches=%d, want=1", npcID, matches)
}
return nil
}
func requireTownNPCPlacementAbsent(archive *platformpvf.Archive, path string, npcID int) error {
raw, err := archive.ReadRaw(path)
if err != nil {
return err
}
inNPCSection := false
for offset := 0; offset+5 <= len(raw); offset += 5 {
token := raw[offset : offset+5]
if token[0] == 3 {
inNPCSection = normalizedLabel(archive.ResolveString(scriptTokenInt(token))) == "npc"
continue
}
if inNPCSection && token[0] == 0 && scriptTokenInt(token) == npcID {
return fmt.Errorf("town map %s still contains NPC %d", path, npcID)
}
}
return nil
}
func normalizedLabel(value string) string {
return strings.ToLower(strings.Trim(strings.TrimSpace(value), "[]"))
}
func scriptTokenInt(token []byte) int {
return int(int32(binary.LittleEndian.Uint32(token[1:5])))
}
func must(err error) {
if err != nil {
panic(err)
}
}