Files
90CNServer/server/internal/modules/dnf/guild/war_shop.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

245 lines
6.3 KiB
Go

package guild
import (
"math"
"path"
"strings"
"time"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
type WarShop struct {
ID, NPC uint32
Items map[uint32]bool
}
type WarMerchant struct {
NPC uint32
Levels map[uint16]WarShop
Costs map[uint16]GuardianCost
}
type WarShopCatalog [2]WarMerchant
func warList(source dnfpvf.Source, listPath string) (map[uint32]string, error) {
doc, err := readWarDocument(source, listPath)
if err != nil {
return nil, err
}
out := make(map[uint32]string)
for _, entry := range dnfpvf.ParseList(doc) {
if entry.ID <= 0 || entry.ID > math.MaxInt32 {
continue
}
out[uint32(entry.ID)] = path.Join(path.Dir(listPath), strings.TrimPrefix(entry.Path, path.Dir(listPath)+"/"))
}
return out, nil
}
func LoadWarShops(source dnfpvf.Source) (WarShopCatalog, error) {
var out WarShopCatalog
doc, err := readWarDocument(source, "etc/agitwar.etc")
if err != nil {
return out, err
}
npcs, err := warList(source, "npc/npc.lst")
if err != nil {
return out, err
}
shops, err := warList(source, "itemshop/itemshop.lst")
if err != nil {
return out, err
}
// Current EXE's upgrade requests identify these original NPCs, while
// purchases identify the level-specific NPC from each [upgrade] row.
for kind, npc := range []uint32{390, 389} {
merchant := WarMerchant{NPC: npc, Levels: make(map[uint16]WarShop), Costs: make(map[uint16]GuardianCost)}
npcdoc, e := readWarDocument(source, npcs[npc])
if e != nil {
return out, e
}
role, _ := npcdoc.Section("guild agit shop npc")
if len(role) != 1 || role[0].Value != []string{"defence", "mercenary"}[kind] {
return out, ErrInvalidContent
}
upgrade, _ := npcdoc.Section("upgrade")
if len(upgrade) < 3 || upgrade[0].Value != "guild agit" {
return out, ErrInvalidContent
}
v, e := warUnsigned(upgrade[1:])
if e != nil || len(v)%2 != 0 || len(v)/2 > 10 {
return out, ErrInvalidContent
}
for i := 0; i < len(v); i += 2 {
if v[i] != uint32(i/2+1) || v[i+1] == 0 {
return out, ErrInvalidContent
}
merchant.Levels[uint16(v[i])] = WarShop{NPC: v[i+1]}
}
inUpgrade := false
for _, sec := range doc.Sections {
if sec.Name == "building upgrade" {
inUpgrade = true
continue
}
if sec.Name == "/building upgrade" {
inUpgrade = false
continue
}
if !inUpgrade || sec.Name != []string{"building", "mecenary"}[kind] {
continue
}
v, e = warUnsigned(doc.Tokens[sec.Start:sec.End])
if e != nil || len(v) != len(merchant.Levels)*2 {
return out, ErrInvalidContent
}
for i := 0; i < len(v); i += 2 {
merchant.Costs[uint16(i/2+1)] = GuardianCost{v[i], v[i+1]}
}
}
if len(merchant.Costs) != len(merchant.Levels) {
return out, ErrInvalidContent
}
out[kind] = merchant
}
tokens, _ := doc.Section("shop list")
ids, err := warUnsigned(tokens)
if err != nil || len(ids) == 0 {
return out, ErrInvalidContent
}
for _, id := range ids {
d, e := readWarDocument(source, shops[id])
if e != nil {
return out, e
}
npc, ok := d.Int("NPC")
typ, _ := d.Section("type")
if !ok || npc <= 0 || len(typ) != 1 || typ[0].Value != "[agitwar build shop]" {
return out, ErrInvalidContent
}
for k := range out {
for level, shop := range out[k].Levels {
if shop.NPC != uint32(npc) {
continue
}
if shop.ID != 0 {
return out, ErrInvalidContent
}
shop.ID = id
shop.Items = make(map[uint32]bool)
for _, sec := range d.Sections {
if sec.Name == "item list" {
v, e := warUnsigned(d.Tokens[sec.Start:sec.End])
if e != nil {
return out, e
}
for _, item := range v {
if item > 0 {
shop.Items[item] = true
}
}
}
}
if len(shop.Items) == 0 {
return out, ErrInvalidContent
}
out[k].Levels[level] = shop
}
}
}
for _, merchant := range out {
for _, shop := range merchant.Levels {
if shop.ID == 0 {
return out, ErrInvalidContent
}
}
}
return out, nil
}
func (g *Guild) WarShopLevel(kind int) uint16 {
if g == nil || kind < 0 || kind >= len(g.WarShopLevels) {
return 0
}
level := g.WarShopLevels[kind]
if level == 0 {
return 1
} // Native 38D1020 constructor: two u16 levels, both 1.
return level
}
func (r *Registry) UpgradeWarShop(characterID string, npc uint32, before, after uint16, catalog WarShopCatalog, now time.Time) (byte, error) {
g, err := r.warManager(characterID)
if err != nil {
return 0, err
}
for kind, merchant := range catalog {
if merchant.NPC != npc {
continue
}
cost, valid := merchant.Costs[after]
if !valid || before == 0 || uint32(after) != uint32(before)+1 {
return 0, ErrInvalidContent
}
current := g.WarShopLevel(kind)
if current == after {
return byte(kind), nil
}
if current != before {
return 0, ErrInvalidContent
}
if g.GuardianAP < cost.AP || g.GuardianGP < cost.GP {
return 0, ErrInsufficientGuardian
}
g.GuardianAP -= cost.AP
g.GuardianGP -= cost.GP
g.WarShopLevels[kind] = after
g.UpdatedAt = now
return byte(kind), nil
}
return 0, ErrInvalidContent
}
func LoadWarProductCost(source dnfpvf.Source, itemPath string) (GuardianCost, error) {
doc, err := readWarDocument(source, itemPath)
if err != nil {
return GuardianCost{}, err
}
ap, apOK := doc.Int("require ap")
gp, gpOK := doc.Int("require gp")
if (!apOK && !gpOK) || ap < 0 || gp < 0 || ap > math.MaxInt32 || gp > math.MaxInt32 || ap+gp == 0 {
return GuardianCost{}, ErrInvalidContent
}
return GuardianCost{uint32(ap), uint32(gp)}, nil
}
func (r *Registry) BuyWarProduct(characterID string, shopID, npc, item, count uint32, catalog WarShopCatalog, cost GuardianCost, now time.Time) error {
g, err := r.warManager(characterID)
if err != nil {
return err
}
if count == 0 || count > math.MaxUint16 || cost.AP == 0 && cost.GP == 0 {
return ErrInvalidContent
}
valid := false
for kind, merchant := range catalog {
shop, ok := merchant.Levels[g.WarShopLevel(kind)]
if ok && shop.ID == shopID && shop.NPC == npc && shop.Items[item] {
valid = true
break
}
}
if !valid {
return ErrInvalidContent
}
ap, gp := uint64(cost.AP)*uint64(count), uint64(cost.GP)*uint64(count)
if ap > uint64(g.GuardianAP) || gp > uint64(g.GuardianGP) {
return ErrInsufficientGuardian
}
g.GuardianAP -= uint32(ap)
g.GuardianGP -= uint32(gp)
g.UpdatedAt = now
return nil
}