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,636 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
batchID = "20260803-level90-strength-loadout"
|
||||
mainListType = byte(0)
|
||||
medalListType = byte(38)
|
||||
extraSlotMask = byte(0x13)
|
||||
topQualitySeed = uint32(999999998)
|
||||
amplifyStamina = byte(1)
|
||||
amplifyStrength = byte(3)
|
||||
amplifyBaseValue = uint16(7)
|
||||
itemRowSize = 0x77
|
||||
)
|
||||
|
||||
type instanceConfig struct {
|
||||
Database struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
} `json:"database"`
|
||||
}
|
||||
|
||||
type itemSpec struct {
|
||||
ListType byte
|
||||
Slot int16
|
||||
ItemID int64
|
||||
Name string
|
||||
PVFPath string
|
||||
Kind string
|
||||
PVFType string
|
||||
Rarity int64
|
||||
Grade int64
|
||||
Durability uint16
|
||||
Count int64
|
||||
UpgradeLevel byte
|
||||
Amplify bool
|
||||
}
|
||||
|
||||
type character struct {
|
||||
ID string
|
||||
AccountID string
|
||||
Name string
|
||||
Level int
|
||||
SlotState byte
|
||||
}
|
||||
|
||||
type amplifyRepairTarget struct {
|
||||
Equipped bool
|
||||
CollectionName string
|
||||
EntryKey string
|
||||
ItemID int64
|
||||
RawEntry []byte
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "../runtime/config/instance.json", "runtime instance config")
|
||||
pvfPath := flag.String("pvf", "../runtime/data/dnf/Script.pvf", "runtime Script.pvf")
|
||||
apply := flag.Bool("apply", false, "commit the validated grant")
|
||||
repairStrength := flag.Bool("repair-strength", false, "repair this batch from dimensional stamina to dimensional strength")
|
||||
flag.Parse()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
specs := requestedItems()
|
||||
if err := validateManifest(*pvfPath, specs); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
db, err := openDatabase(cfg)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
fatal(fmt.Errorf("ping database: %w", err))
|
||||
}
|
||||
|
||||
if *repairStrength {
|
||||
result, repaired, repairErr := repairBatchStrength(ctx, db, specs, *apply)
|
||||
if repairErr != nil {
|
||||
fatal(repairErr)
|
||||
}
|
||||
mode := "DRY-RUN"
|
||||
if *apply {
|
||||
mode = "APPLIED"
|
||||
}
|
||||
fmt.Printf("%s strength-repair character=%s id=%s level=%d rows=%d old_type=%d new_type=%d batch=%s\n",
|
||||
mode, result.Name, result.ID, result.Level, repaired, amplifyStamina, amplifyStrength, batchID)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := grant(ctx, db, specs, *apply)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
mode := "DRY-RUN"
|
||||
if *apply {
|
||||
mode = "APPLIED"
|
||||
}
|
||||
fmt.Printf("%s character=%s id=%s level=%d old_slot_mask=%#x new_slot_mask=%#x rows=%d main=%d medals=%d guardian_gem_stacks=%d guardian_gem_units=%d batch=%s\n",
|
||||
mode, result.Name, result.ID, result.Level, result.SlotState, result.SlotState|extraSlotMask,
|
||||
len(specs), countList(specs, mainListType), countMedals(specs), countGuardianGems(specs), guardianGemUnits(specs), batchID)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (instanceConfig, error) {
|
||||
var cfg instanceConfig
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("read instance config: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parse instance config: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(cfg.Database.Host) == "" || cfg.Database.Port <= 0 || strings.TrimSpace(cfg.Database.User) == "" || strings.TrimSpace(cfg.Database.Name) == "" {
|
||||
return cfg, errors.New("instance config has incomplete database settings")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func openDatabase(cfg instanceConfig) (*sql.DB, error) {
|
||||
mysqlConfig := mysql.Config{
|
||||
User: cfg.Database.User,
|
||||
Passwd: cfg.Database.Password,
|
||||
Net: "tcp",
|
||||
Addr: net.JoinHostPort(cfg.Database.Host, strconv.Itoa(cfg.Database.Port)),
|
||||
DBName: cfg.Database.Name,
|
||||
ParseTime: true,
|
||||
Loc: time.UTC,
|
||||
Params: map[string]string{"charset": "utf8mb4"},
|
||||
}
|
||||
dsn := mysqlConfig.FormatDSN()
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func grant(ctx context.Context, db *sql.DB, specs []itemSpec, apply bool) (character, error) {
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||
if err != nil {
|
||||
return character{}, fmt.Errorf("begin grant transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `SELECT character_id, account_id, name, level, ex_equip_slot_stat FROM dnf_characters WHERE level = 90 AND delete_flag = 0 FOR UPDATE`)
|
||||
if err != nil {
|
||||
return character{}, fmt.Errorf("lock level-90 character: %w", err)
|
||||
}
|
||||
var characters []character
|
||||
for rows.Next() {
|
||||
var row character
|
||||
if err := rows.Scan(&row.ID, &row.AccountID, &row.Name, &row.Level, &row.SlotState); err != nil {
|
||||
rows.Close()
|
||||
return character{}, fmt.Errorf("scan level-90 character: %w", err)
|
||||
}
|
||||
characters = append(characters, row)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return character{}, fmt.Errorf("close character rows: %w", err)
|
||||
}
|
||||
if len(characters) != 1 {
|
||||
return character{}, fmt.Errorf("expected exactly one active level-90 character, found %d", len(characters))
|
||||
}
|
||||
selected := characters[0]
|
||||
|
||||
occupiedRows, err := tx.QueryContext(ctx, `SELECT entry_key FROM dnf_inventory_items WHERE character_id = ? AND collection_name = 'slots' FOR UPDATE`, selected.ID)
|
||||
if err != nil {
|
||||
return character{}, fmt.Errorf("lock inventory: %w", err)
|
||||
}
|
||||
occupied := make(map[string]struct{})
|
||||
for occupiedRows.Next() {
|
||||
var key string
|
||||
if err := occupiedRows.Scan(&key); err != nil {
|
||||
occupiedRows.Close()
|
||||
return character{}, fmt.Errorf("scan occupied inventory slot: %w", err)
|
||||
}
|
||||
occupied[key] = struct{}{}
|
||||
}
|
||||
if err := occupiedRows.Close(); err != nil {
|
||||
return character{}, fmt.Errorf("close inventory rows: %w", err)
|
||||
}
|
||||
for _, spec := range specs {
|
||||
key := itemKey(spec)
|
||||
if _, exists := occupied[key]; exists {
|
||||
return character{}, fmt.Errorf("required destination slot %s is occupied", key)
|
||||
}
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return selected, nil
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE dnf_characters SET ex_equip_slot_stat = ex_equip_slot_stat | ?, updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, extraSlotMask, selected.ID); err != nil {
|
||||
return character{}, fmt.Errorf("unlock extra equipment slots: %w", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE dnf_inventories SET updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, selected.ID); err != nil {
|
||||
return character{}, fmt.Errorf("touch inventory: %w", err)
|
||||
}
|
||||
for _, spec := range specs {
|
||||
if err := insertItem(ctx, tx, selected.ID, spec); err != nil {
|
||||
return character{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return character{}, fmt.Errorf("commit loadout grant: %w", err)
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func repairBatchStrength(ctx context.Context, db *sql.DB, specs []itemSpec, apply bool) (character, int, error) {
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("begin amplification repair transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `SELECT character_id, account_id, name, level, ex_equip_slot_stat FROM dnf_characters WHERE level = 90 AND delete_flag = 0 FOR UPDATE`)
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("lock level-90 character: %w", err)
|
||||
}
|
||||
var characters []character
|
||||
for rows.Next() {
|
||||
var row character
|
||||
if err := rows.Scan(&row.ID, &row.AccountID, &row.Name, &row.Level, &row.SlotState); err != nil {
|
||||
rows.Close()
|
||||
return character{}, 0, fmt.Errorf("scan level-90 character: %w", err)
|
||||
}
|
||||
characters = append(characters, row)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return character{}, 0, fmt.Errorf("close character rows: %w", err)
|
||||
}
|
||||
if len(characters) != 1 {
|
||||
return character{}, 0, fmt.Errorf("expected exactly one active level-90 character, found %d", len(characters))
|
||||
}
|
||||
selected := characters[0]
|
||||
|
||||
targets, err := loadAmplifyRepairTargets(ctx, tx, selected.ID)
|
||||
if err != nil {
|
||||
return character{}, 0, err
|
||||
}
|
||||
expected := make(map[int64]struct{}, 16)
|
||||
for _, spec := range specs {
|
||||
if spec.Amplify {
|
||||
expected[spec.ItemID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(targets) != len(expected) {
|
||||
return character{}, 0, fmt.Errorf("expected %d stamina-amplified batch rows, found %d", len(expected), len(targets))
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
if _, ok := expected[target.ItemID]; !ok {
|
||||
return character{}, 0, fmt.Errorf("unexpected batch item %d at %s", target.ItemID, target.EntryKey)
|
||||
}
|
||||
if _, duplicate := seen[target.ItemID]; duplicate {
|
||||
return character{}, 0, fmt.Errorf("duplicate batch item %d", target.ItemID)
|
||||
}
|
||||
seen[target.ItemID] = struct{}{}
|
||||
if len(target.RawEntry) != itemRowSize || target.RawEntry[0x0A]&0x1F != 13 || target.RawEntry[0x13] != amplifyStamina || binary.LittleEndian.Uint16(target.RawEntry[0x14:0x16]) != amplifyBaseValue {
|
||||
return character{}, 0, fmt.Errorf("item %d at %s has unexpected raw amplification state", target.ItemID, target.EntryKey)
|
||||
}
|
||||
}
|
||||
if !apply {
|
||||
return selected, len(targets), nil
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
raw := append([]byte(nil), target.RawEntry...)
|
||||
raw[0x13] = amplifyStrength
|
||||
if target.Equipped {
|
||||
result, err := tx.ExecContext(ctx, `UPDATE dnf_equipment_entries SET raw_entry = ? WHERE character_id = ? AND entry_key = ? AND item_id = ?`, raw, selected.ID, target.EntryKey, target.ItemID)
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("update equipped raw item %d: %w", target.ItemID, err)
|
||||
}
|
||||
if err := requireAffected(result, 1, "equipped raw", target.ItemID); err != nil {
|
||||
return character{}, 0, err
|
||||
}
|
||||
result, err = tx.ExecContext(ctx, `UPDATE dnf_equipment_entry_extra SET extra_value = ? WHERE character_id = ? AND entry_key = ? AND extra_key IN ('amplify_type','amplification_type','byte_13','value_13','value_c') AND extra_value = ?`,
|
||||
strconv.Itoa(int(amplifyStrength)), selected.ID, target.EntryKey, strconv.Itoa(int(amplifyStamina)))
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("update equipped amplification metadata item %d: %w", target.ItemID, err)
|
||||
}
|
||||
if err := requireAffected(result, 5, "equipped amplification metadata", target.ItemID); err != nil {
|
||||
return character{}, 0, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `UPDATE dnf_inventory_items SET raw_entry = ? WHERE character_id = ? AND collection_name = ? AND entry_key = ? AND item_id = ?`,
|
||||
raw, selected.ID, target.CollectionName, target.EntryKey, target.ItemID)
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("update inventory raw item %d: %w", target.ItemID, err)
|
||||
}
|
||||
if err := requireAffected(result, 1, "inventory raw", target.ItemID); err != nil {
|
||||
return character{}, 0, err
|
||||
}
|
||||
result, err = tx.ExecContext(ctx, `UPDATE dnf_inventory_item_extra SET extra_value = ? WHERE character_id = ? AND collection_name = ? AND entry_key = ? AND extra_key IN ('amplify_type','amplification_type','byte_13','value_13','value_c') AND extra_value = ?`,
|
||||
strconv.Itoa(int(amplifyStrength)), selected.ID, target.CollectionName, target.EntryKey, strconv.Itoa(int(amplifyStamina)))
|
||||
if err != nil {
|
||||
return character{}, 0, fmt.Errorf("update inventory amplification metadata item %d: %w", target.ItemID, err)
|
||||
}
|
||||
if err := requireAffected(result, 5, "inventory amplification metadata", target.ItemID); err != nil {
|
||||
return character{}, 0, err
|
||||
}
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE dnf_inventories SET updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, selected.ID); err != nil {
|
||||
return character{}, 0, fmt.Errorf("touch inventory after amplification repair: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return character{}, 0, fmt.Errorf("commit amplification repair: %w", err)
|
||||
}
|
||||
return selected, len(targets), nil
|
||||
}
|
||||
|
||||
func loadAmplifyRepairTargets(ctx context.Context, tx *sql.Tx, characterID string) ([]amplifyRepairTarget, error) {
|
||||
var targets []amplifyRepairTarget
|
||||
rows, err := tx.QueryContext(ctx, `SELECT i.collection_name, i.entry_key, i.item_id, i.raw_entry FROM dnf_inventory_items i JOIN dnf_inventory_item_extra b ON b.character_id=i.character_id AND b.collection_name=i.collection_name AND b.entry_key=i.entry_key AND b.extra_key='grant_batch' AND b.extra_value=? JOIN dnf_inventory_item_extra a ON a.character_id=i.character_id AND a.collection_name=i.collection_name AND a.entry_key=i.entry_key AND a.extra_key='amplify_type' AND a.extra_value=? WHERE i.character_id=? FOR UPDATE`,
|
||||
batchID, strconv.Itoa(int(amplifyStamina)), characterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lock inventory amplification rows: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var target amplifyRepairTarget
|
||||
if err := rows.Scan(&target.CollectionName, &target.EntryKey, &target.ItemID, &target.RawEntry); err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("scan inventory amplification row: %w", err)
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close inventory amplification rows: %w", err)
|
||||
}
|
||||
|
||||
rows, err = tx.QueryContext(ctx, `SELECT e.entry_key, e.item_id, e.raw_entry FROM dnf_equipment_entries e JOIN dnf_equipment_entry_extra b ON b.character_id=e.character_id AND b.entry_key=e.entry_key AND b.extra_key='grant_batch' AND b.extra_value=? JOIN dnf_equipment_entry_extra a ON a.character_id=e.character_id AND a.entry_key=e.entry_key AND a.extra_key='amplify_type' AND a.extra_value=? WHERE e.character_id=? FOR UPDATE`,
|
||||
batchID, strconv.Itoa(int(amplifyStamina)), characterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lock equipped amplification rows: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
target := amplifyRepairTarget{Equipped: true}
|
||||
if err := rows.Scan(&target.EntryKey, &target.ItemID, &target.RawEntry); err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("scan equipped amplification row: %w", err)
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close equipped amplification rows: %w", err)
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func requireAffected(result sql.Result, want int64, field string, itemID int64) error {
|
||||
got, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read affected rows for %s item %d: %w", field, itemID, err)
|
||||
}
|
||||
if got != want {
|
||||
return fmt.Errorf("%s item %d affected %d rows, want %d", field, itemID, got, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertItem(ctx context.Context, tx *sql.Tx, characterID string, spec itemSpec) error {
|
||||
key := itemKey(spec)
|
||||
raw := buildRaw(spec)
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO dnf_inventory_items (character_id, collection_name, entry_key, item_id, item_count, bind_flag, expire_at, raw_entry) VALUES (?, 'slots', ?, ?, ?, 0, NULL, ?)`,
|
||||
characterID, key, spec.ItemID, spec.Count, raw); err != nil {
|
||||
return fmt.Errorf("insert %s (%d) at %s: %w", spec.Name, spec.ItemID, key, err)
|
||||
}
|
||||
extra := itemExtra(spec)
|
||||
keys := make([]string, 0, len(extra))
|
||||
for extraKey := range extra {
|
||||
keys = append(keys, extraKey)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, extraKey := range keys {
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO dnf_inventory_item_extra (character_id, collection_name, entry_key, extra_key, extra_value) VALUES (?, 'slots', ?, ?, ?)`,
|
||||
characterID, key, extraKey, extra[extraKey]); err != nil {
|
||||
return fmt.Errorf("insert metadata %s for %s at %s: %w", extraKey, spec.Name, key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRaw(spec itemSpec) []byte {
|
||||
raw := make([]byte, itemRowSize)
|
||||
binary.LittleEndian.PutUint16(raw[0x00:0x02], uint16(spec.Slot))
|
||||
binary.LittleEndian.PutUint32(raw[0x02:0x06], uint32(spec.ItemID))
|
||||
amount := uint32(spec.Count)
|
||||
if spec.ListType == mainListType && spec.Kind == "equipment" {
|
||||
amount = topQualitySeed
|
||||
}
|
||||
binary.LittleEndian.PutUint32(raw[0x06:0x0A], amount)
|
||||
raw[0x0A] = spec.UpgradeLevel & 0x1F
|
||||
binary.LittleEndian.PutUint16(raw[0x0B:0x0D], spec.Durability)
|
||||
if spec.Kind == "stackable" {
|
||||
binary.LittleEndian.PutUint32(raw[0x0E:0x12], uint32(spec.ItemID))
|
||||
}
|
||||
if spec.Amplify {
|
||||
raw[0x13] = amplifyStrength
|
||||
binary.LittleEndian.PutUint16(raw[0x14:0x16], amplifyBaseValue)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func itemExtra(spec itemSpec) map[string]string {
|
||||
extra := map[string]string{
|
||||
"source": "operator_direct_database_grant",
|
||||
"grant_batch": batchID,
|
||||
"item_name": spec.Name,
|
||||
"item_kind": spec.Kind,
|
||||
"pvf_path": spec.PVFPath,
|
||||
}
|
||||
if spec.PVFType != "" {
|
||||
if spec.Kind == "equipment" {
|
||||
extra["equipment_type"] = spec.PVFType
|
||||
} else {
|
||||
extra["stackable_type"] = spec.PVFType
|
||||
}
|
||||
}
|
||||
if spec.Kind == "equipment" {
|
||||
extra["durability"] = strconv.Itoa(int(spec.Durability))
|
||||
extra["max_durability"] = strconv.Itoa(int(spec.Durability))
|
||||
if spec.ListType == mainListType {
|
||||
extra["quality_seed"] = strconv.FormatUint(uint64(topQualitySeed), 10)
|
||||
}
|
||||
if spec.UpgradeLevel != 0 {
|
||||
level := strconv.Itoa(int(spec.UpgradeLevel))
|
||||
extra["ext_data0"] = level
|
||||
extra["packed_flag_byte"] = level
|
||||
extra["reinforce"] = level
|
||||
extra["upgrade_level"] = level
|
||||
}
|
||||
} else {
|
||||
count := strconv.FormatInt(spec.Count, 10)
|
||||
extra["amount"] = count
|
||||
extra["amount_or_count"] = count
|
||||
extra["count"] = count
|
||||
extra["value_a"] = strconv.FormatInt(spec.ItemID, 10)
|
||||
}
|
||||
if spec.Amplify {
|
||||
typeValue := strconv.Itoa(int(amplifyStrength))
|
||||
baseValue := strconv.Itoa(int(amplifyBaseValue))
|
||||
for _, key := range []string{"amplify_type", "amplification_type", "byte_13", "value_13", "value_c"} {
|
||||
extra[key] = typeValue
|
||||
}
|
||||
for _, key := range []string{"amplify_value", "amplification_value", "marker_16", "marker16", "value_d"} {
|
||||
extra[key] = baseValue
|
||||
}
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
func itemKey(spec itemSpec) string {
|
||||
return fmt.Sprintf("%d:%d", spec.ListType, spec.Slot)
|
||||
}
|
||||
|
||||
func validateManifest(pvfPath string, specs []itemSpec) error {
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open runtime PVF: %w", err)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(specs))
|
||||
for _, spec := range specs {
|
||||
key := itemKey(spec)
|
||||
if _, exists := seen[key]; exists {
|
||||
return fmt.Errorf("duplicate destination slot %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
text, err := archive.ReadText(spec.PVFPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read PVF item %d at %s: %w", spec.ItemID, spec.PVFPath, err)
|
||||
}
|
||||
doc, err := dnfpvf.Parse(spec.PVFPath, text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse PVF item %d at %s: %w", spec.ItemID, spec.PVFPath, err)
|
||||
}
|
||||
name, _ := doc.Text("name")
|
||||
if name != spec.Name {
|
||||
return fmt.Errorf("PVF name mismatch item %d: got %q want %q", spec.ItemID, name, spec.Name)
|
||||
}
|
||||
section := "equipment type"
|
||||
if spec.Kind == "stackable" {
|
||||
section = "stackable type"
|
||||
}
|
||||
pvfType, _ := doc.Text(section)
|
||||
if !strings.EqualFold(strings.TrimSpace(pvfType), spec.PVFType) {
|
||||
return fmt.Errorf("PVF type mismatch item %d: got %q want %q", spec.ItemID, pvfType, spec.PVFType)
|
||||
}
|
||||
rarity, _ := doc.Int("rarity")
|
||||
if rarity != spec.Rarity {
|
||||
return fmt.Errorf("PVF rarity mismatch item %d: got %d want %d", spec.ItemID, rarity, spec.Rarity)
|
||||
}
|
||||
if spec.Grade != 0 {
|
||||
grade, _ := doc.Int("grade")
|
||||
if grade != spec.Grade {
|
||||
return fmt.Errorf("PVF grade mismatch item %d: got %d want %d", spec.ItemID, grade, spec.Grade)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requestedItems() []itemSpec {
|
||||
main := []itemSpec{
|
||||
{ItemID: 100070551, Name: "超大陆 - 瓦巴拉的大地", PVFPath: "equipment/character/common/jacket/larmor/100070551.equ", PVFType: "[coat]", Rarity: 4, Durability: 38},
|
||||
{ItemID: 100120544, Name: "超大陆 - 盘古大陆的地震", PVFPath: "equipment/character/common/pants/larmor/100120544.equ", PVFType: "[pants]", Rarity: 4, Durability: 32},
|
||||
{ItemID: 100170519, Name: "超大陆 - 潘诺西亚的火山", PVFPath: "equipment/character/common/shoulder/larmor/100170519.equ", PVFType: "[shoulder]", Rarity: 4, Durability: 30},
|
||||
{ItemID: 100220519, Name: "超大陆 - 罗迪尼亚的熔岩", PVFPath: "equipment/character/common/belt/larmor/100220519.equ", PVFType: "[waist]", Rarity: 4, Durability: 25},
|
||||
{ItemID: 100270515, Name: "超大陆 - 凯诺兰的地壳", PVFPath: "equipment/character/common/shoes/larmor/100270515.equ", PVFType: "[shoes]", Rarity: 4, Durability: 25},
|
||||
{ItemID: 100300733, Name: "氤氲之息", PVFPath: "equipment/character/common/amulet/100300733.equ", PVFType: "[amulet]", Rarity: 4},
|
||||
{ItemID: 100312425, Name: "启明星的指引", PVFPath: "equipment/character/common/wrist/100312425.equ", PVFType: "[wrist]", Rarity: 4},
|
||||
{ItemID: 100322294, Name: "清泉流响", PVFPath: "equipment/character/common/ring/100322294.equ", PVFType: "[ring]", Rarity: 4},
|
||||
{ItemID: 100390011, Name: "黑暗祭礼", PVFPath: "equipment/character/common/earring/100390011.equ", PVFType: "[earring]", Rarity: 4},
|
||||
{ItemID: 100344511, Name: "波利斯的黄金杯", PVFPath: "equipment/character/common/support/100344511.equ", PVFType: "[support]", Rarity: 4},
|
||||
{ItemID: 100352822, Name: "罗塞塔石碑", PVFPath: "equipment/character/common/magicstone/100352822.equ", PVFType: "[magic stone]", Rarity: 4},
|
||||
{ItemID: 100390003, Name: "英雄王的象征", PVFPath: "equipment/character/common/earring/100390003.equ", PVFType: "[earring]", Rarity: 4},
|
||||
{ItemID: 100344527, Name: "王冠非冠", PVFPath: "equipment/character/common/support/100344527.equ", PVFType: "[support]", Rarity: 4},
|
||||
{ItemID: 100352839, Name: "王座本源", PVFPath: "equipment/character/common/magicstone/100352839.equ", PVFType: "[magic stone]", Rarity: 4},
|
||||
{ItemID: 101010864, Name: "圣耀救赎太刀", PVFPath: "equipment/character/swordman/weapon/katana/101010864.equ", PVFType: "[weapon]", Rarity: 4, Durability: 45},
|
||||
{ItemID: 101030741, Name: "圣耀救赎巨剑", PVFPath: "equipment/character/swordman/weapon/hsword/101030741.equ", PVFType: "[weapon]", Rarity: 4, Durability: 45},
|
||||
{ItemID: 400330106, Name: "天选之人", PVFPath: "equipment/character/common/title/chn_400330106.equ", PVFType: "[title name]", Rarity: 6},
|
||||
}
|
||||
for index := range main {
|
||||
main[index].ListType = mainListType
|
||||
main[index].Slot = int16(9 + index)
|
||||
main[index].Kind = "equipment"
|
||||
main[index].Count = 1
|
||||
if index < len(main)-1 {
|
||||
main[index].UpgradeLevel = 13
|
||||
main[index].Amplify = true
|
||||
}
|
||||
}
|
||||
|
||||
medalNames := []string{
|
||||
"完美的野兽图腾勋章", "完美的神速图腾勋章", "完美的斗志图腾勋章", "完美的集中图腾勋章", "完美的减速图腾勋章", "完美的协作图腾勋章", "完美的再生图腾勋章",
|
||||
"完美的守护图腾勋章", "完美的祝福图腾勋章", "完美的惩罚图腾勋章", "完美的元气图腾勋章", "完美的觉醒图腾勋章", "完美的坚韧图腾勋章",
|
||||
}
|
||||
medals := make([]itemSpec, 0, len(medalNames))
|
||||
for index, name := range medalNames {
|
||||
itemID := int64(100380051 + index)
|
||||
medals = append(medals, itemSpec{
|
||||
ListType: medalListType, Slot: int16(index), ItemID: itemID, Name: name,
|
||||
PVFPath: fmt.Sprintf("equipment/character/common/flag/%d.equ", itemID), Kind: "equipment", PVFType: "[flag]",
|
||||
Rarity: 6, Grade: 55, Count: 1, UpgradeLevel: 16,
|
||||
})
|
||||
}
|
||||
|
||||
gemIDs := []int64{90003, 90013, 90023, 90033, 90043, 90053, 90063, 90073, 90083, 90093, 90103, 90113, 90123}
|
||||
gemNames := []string{
|
||||
"玲珑之光守护珠 (物理防御力)", "玲珑之光守护珠 (魔法防御力)", "玲珑之光守护珠 (攻击速度)", "玲珑之光守护珠 (移动速度)", "玲珑之光守护珠 (施放速度)",
|
||||
"玲珑之光守护珠 (物理暴击)", "玲珑之光守护珠 (魔法暴击)", "玲珑之光守护珠 (火属性强化)", "玲珑之光守护珠 (冰属性强化)", "玲珑之光守护珠 (暗属性强化)",
|
||||
"玲珑之光守护珠 (光属性强化)", "玲珑之光守护珠 (命中率)", "玲珑之光守护珠 (负重上限)",
|
||||
}
|
||||
gems := make([]itemSpec, 0, len(gemIDs))
|
||||
for index, itemID := range gemIDs {
|
||||
gems = append(gems, itemSpec{
|
||||
ListType: medalListType, Slot: int16(49 + index), ItemID: itemID, Name: gemNames[index],
|
||||
PVFPath: fmt.Sprintf("stackable/flaggem/%d.stk", itemID), Kind: "stackable", PVFType: "[flag gem]",
|
||||
Rarity: 3, Grade: 3, Count: 10,
|
||||
})
|
||||
}
|
||||
return append(append(main, medals...), gems...)
|
||||
}
|
||||
|
||||
func countList(specs []itemSpec, listType byte) int {
|
||||
count := 0
|
||||
for _, spec := range specs {
|
||||
if spec.ListType == listType {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func countMedals(specs []itemSpec) int {
|
||||
count := 0
|
||||
for _, spec := range specs {
|
||||
if spec.PVFType == "[flag]" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func countGuardianGems(specs []itemSpec) int {
|
||||
count := 0
|
||||
for _, spec := range specs {
|
||||
if spec.PVFType == "[flag gem]" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func guardianGemUnits(specs []itemSpec) int64 {
|
||||
var count int64
|
||||
for _, spec := range specs {
|
||||
if spec.PVFType == "[flag gem]" {
|
||||
count += spec.Count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, "grantlevel90loadout:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in new issue
Block a user