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,275 @@
|
||||
// Command pvfjoustentry prepares a candidate Script.pvf that places the
|
||||
// existing 2017 Chinese joust NPC at a map-owned legacy event-NPC slot. The
|
||||
// server event catalog owns all-day activation; the NPC's native event gates
|
||||
// must stay present so the current client enables its joust role action. It
|
||||
// never edits the active PVF in place.
|
||||
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 (
|
||||
sourceNPCPath = "npc/chn_masang_2017.npc"
|
||||
targetNPCPath = "npc/chn_event_lions10th_2.npc"
|
||||
sourceShopPath = "itemshop/chn_masang_shop_2017.shp"
|
||||
targetShopPath = "itemshop/chn_10th_lions_2.shp"
|
||||
itemShopListPath = "itemshop/itemshop.lst"
|
||||
mapPath = "map/cataclysm/town/hendonmyre/new_hendon_main.map"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
InputPath string `json:"input_path"`
|
||||
OutputPath string `json:"output_path"`
|
||||
SourceNPC string `json:"source_npc"`
|
||||
TargetNPC string `json:"target_npc"`
|
||||
TargetShop string `json:"target_shop"`
|
||||
TownMap string `json:"town_map"`
|
||||
TownNPCID int `json:"town_npc_id"`
|
||||
TownX int `json:"town_x"`
|
||||
TownY int `json:"town_y"`
|
||||
PreservedSections []string `json:"preserved_sections"`
|
||||
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/Script-joust-entry-candidate.pvf`, "candidate output path")
|
||||
flag.Parse()
|
||||
|
||||
archive, err := platformpvf.Open(*pvfPath)
|
||||
must(err)
|
||||
sourceText, err := archive.ReadText(sourceNPCPath)
|
||||
must(err)
|
||||
sourceShopText, err := archive.ReadText(sourceShopPath)
|
||||
must(err)
|
||||
itemShopListText, err := archive.ReadText(itemShopListPath)
|
||||
must(err)
|
||||
mapText, err := archive.ReadText(mapPath)
|
||||
must(err)
|
||||
|
||||
requireContains(sourceNPCPath, sourceText,
|
||||
"`[joust event]` 0",
|
||||
"`[item shop]` 101262",
|
||||
"[open visible event]\n2365",
|
||||
"[event open]\n2365 -1 -1",
|
||||
)
|
||||
requireContains(sourceShopPath, sourceShopText,
|
||||
"490005585", "490005586", "490005588", "490005589",
|
||||
"490005590", "490005591", "490005592",
|
||||
)
|
||||
requireContains(itemShopListPath, itemShopListText, "400000022", "chn_10th_lions_2.shp")
|
||||
if !strings.Contains(mapText, "20449 `[left]` 1478 354 0") &&
|
||||
!strings.Contains(mapText, "20449 `[left]` 2183 156 0") {
|
||||
panic(fmt.Errorf("%s has neither the original nor midpoint joust NPC tuple", mapPath))
|
||||
}
|
||||
|
||||
sourceRaw, err := archive.ReadRaw(sourceNPCPath)
|
||||
must(err)
|
||||
placedNPCRaw, err := replaceSectionIntValue(archive, sourceRaw, "item shop", 101262, 400000022)
|
||||
must(err)
|
||||
sourceShopRaw, err := archive.ReadRaw(sourceShopPath)
|
||||
must(err)
|
||||
placedShopRaw, err := replaceSectionIntValue(archive, sourceShopRaw, "npc", 20325, 20449)
|
||||
must(err)
|
||||
mapRaw, err := archive.ReadRaw(mapPath)
|
||||
must(err)
|
||||
placedMapRaw, err := ensureTownNPCPlacement(archive, mapRaw, 20449, 1478, 354, 2183, 156)
|
||||
must(err)
|
||||
|
||||
patched, err := archive.RepackRaw([]platformpvf.RawReplacement{
|
||||
{Path: targetNPCPath, Data: placedNPCRaw},
|
||||
{Path: targetShopPath, Data: placedShopRaw},
|
||||
{Path: mapPath, Data: placedMapRaw},
|
||||
})
|
||||
must(err)
|
||||
verified, err := platformpvf.OpenBytes(patched)
|
||||
must(err)
|
||||
verifiedText, err := verified.ReadText(targetNPCPath)
|
||||
must(err)
|
||||
requireContains(targetNPCPath, verifiedText,
|
||||
"`[joust event]` 0",
|
||||
"`[item shop]` 400000022",
|
||||
"[open visible event]\n2365",
|
||||
"[event open]\n2365 -1 -1",
|
||||
)
|
||||
for _, forbidden := range []string{"101262"} {
|
||||
if strings.Contains(verifiedText, forbidden) {
|
||||
panic(fmt.Errorf("verified target %s still contains %q", targetNPCPath, forbidden))
|
||||
}
|
||||
}
|
||||
verifiedSource, err := verified.ReadText(sourceNPCPath)
|
||||
must(err)
|
||||
if verifiedSource != sourceText {
|
||||
panic(fmt.Errorf("source NPC %s changed", sourceNPCPath))
|
||||
}
|
||||
verifiedShop, err := verified.ReadText(targetShopPath)
|
||||
must(err)
|
||||
requireContains(targetShopPath, verifiedShop,
|
||||
"490005585", "490005586", "490005588", "490005589",
|
||||
"490005590", "490005591", "490005592",
|
||||
)
|
||||
must(requireSectionIntValue(verified, targetShopPath, "npc", 20449))
|
||||
verifiedItemShopList, err := verified.ReadText(itemShopListPath)
|
||||
must(err)
|
||||
if verifiedItemShopList != itemShopListText {
|
||||
panic(fmt.Errorf("indexed item-shop list %s changed", itemShopListPath))
|
||||
}
|
||||
verifiedMap, err := verified.ReadText(mapPath)
|
||||
must(err)
|
||||
requireContains(mapPath, verifiedMap,
|
||||
"159 `[left]` 1896 156 0",
|
||||
"20449 `[left]` 2183 156 0",
|
||||
"45 `[right]` 2470 156 0",
|
||||
)
|
||||
if strings.Contains(verifiedMap, "20449 `[left]` 1478 354 0") {
|
||||
panic(fmt.Errorf("verified town map retained the old joust NPC placement"))
|
||||
}
|
||||
|
||||
must(os.MkdirAll(filepath.Dir(*outPath), 0o755))
|
||||
must(os.WriteFile(*outPath, patched, 0o644))
|
||||
inputInfo, err := os.Stat(*pvfPath)
|
||||
must(err)
|
||||
sum := sha256.Sum256(patched)
|
||||
result := report{
|
||||
InputPath: *pvfPath,
|
||||
OutputPath: *outPath,
|
||||
SourceNPC: sourceNPCPath,
|
||||
TargetNPC: targetNPCPath,
|
||||
TargetShop: targetShopPath,
|
||||
TownMap: mapPath,
|
||||
TownNPCID: 20449,
|
||||
TownX: 2183,
|
||||
TownY: 156,
|
||||
PreservedSections: []string{"open visible event", "event open"},
|
||||
InputSize: inputInfo.Size(),
|
||||
OutputSize: len(patched),
|
||||
OutputSHA256: strings.ToUpper(hex.EncodeToString(sum[:])),
|
||||
}
|
||||
encoded, err := json.MarshalIndent(result, "", " ")
|
||||
must(err)
|
||||
fmt.Println(string(encoded))
|
||||
}
|
||||
|
||||
func ensureTownNPCPlacement(archive *platformpvf.Archive, raw []byte, npcID, oldX, oldY, newX, newY int) ([]byte, error) {
|
||||
if archive == nil || len(raw)%5 != 0 {
|
||||
return nil, fmt.Errorf("map token stream is invalid: bytes=%d", len(raw))
|
||||
}
|
||||
out := append([]byte(nil), raw...)
|
||||
inNPCSection := false
|
||||
matches := 0
|
||||
for offset := 0; offset < len(out); offset += 5 {
|
||||
token := out[offset : offset+5]
|
||||
if token[0] == 3 {
|
||||
label := normalizedLabel(archive.ResolveString(scriptTokenInt(token)))
|
||||
inNPCSection = label == "npc"
|
||||
continue
|
||||
}
|
||||
if !inNPCSection || token[0] != 0 || scriptTokenInt(token) != npcID || offset+25 > len(out) {
|
||||
continue
|
||||
}
|
||||
direction := out[offset+5 : offset+10]
|
||||
xToken := out[offset+10 : offset+15]
|
||||
yToken := out[offset+15 : offset+20]
|
||||
zToken := out[offset+20 : offset+25]
|
||||
if direction[0] != 6 || normalizedLabel(archive.ResolveString(scriptTokenInt(direction))) != "left" ||
|
||||
xToken[0] != 0 || yToken[0] != 0 || zToken[0] != 0 || scriptTokenInt(zToken) != 0 {
|
||||
return nil, fmt.Errorf("town NPC %d placement does not match the proved tuple", npcID)
|
||||
}
|
||||
x, y := scriptTokenInt(xToken), scriptTokenInt(yToken)
|
||||
switch {
|
||||
case x == oldX && y == oldY:
|
||||
binary.LittleEndian.PutUint32(xToken[1:5], uint32(int32(newX)))
|
||||
binary.LittleEndian.PutUint32(yToken[1:5], uint32(int32(newY)))
|
||||
case x == newX && y == newY:
|
||||
default:
|
||||
return nil, fmt.Errorf("town NPC %d placement=(%d,%d), want original=(%d,%d) or midpoint=(%d,%d)", npcID, x, y, oldX, oldY, newX, newY)
|
||||
}
|
||||
matches++
|
||||
}
|
||||
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 proved integer %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 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] value does not match required integer %d", section, want)
|
||||
}
|
||||
matches++
|
||||
}
|
||||
if matches != 1 {
|
||||
return fmt.Errorf("section [%s] matches=%d, want=1", section, matches)
|
||||
}
|
||||
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 requireContains(path, text string, values ...string) {
|
||||
for _, value := range values {
|
||||
if !strings.Contains(text, value) {
|
||||
panic(fmt.Errorf("%s is missing required evidence %q", path, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user