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,466 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
currentNPCShopItemListPath = "itemshop/itemshop.lst"
|
||||
currentNPCShopPriceTablePath = "equipment/pricetable.tbl"
|
||||
currentNPCTradingSystemPath = "etc/tradingsystem.etc"
|
||||
)
|
||||
|
||||
var (
|
||||
errCurrentNPCShopCatalogUnavailable = errors.New("dnf current NPC shop PVF catalog is unavailable")
|
||||
errCurrentNPCShopProductUnavailable = errors.New("dnf current NPC shop product is unavailable")
|
||||
)
|
||||
|
||||
type currentNPCShopCatalog struct {
|
||||
source dnfpvf.Source
|
||||
buyableItems map[uint32]struct{}
|
||||
// shopItems 保留每个商店 [item list] 的原始顺序(槽位下标 = 序号),供 op715
|
||||
// 的限购计数表按槽位索引下发。
|
||||
shopItems map[uint32][]uint32
|
||||
// shopPaths 是 itemshop.lst 里 shop id → .shp 文件名的映射,只用于诊断输出。
|
||||
shopPaths map[uint32]string
|
||||
tradingShops map[uint32]currentNPCTradingShop
|
||||
equipmentSellRatePermille int64
|
||||
listedShopCount int
|
||||
loadedShopCount int
|
||||
missingShopCount int
|
||||
firstMissingShopPath string
|
||||
}
|
||||
|
||||
// currentNPCTradingShop is the PVF-owned special buy/sell/exchange table used
|
||||
// by the time-stone NPCs. It is deliberately kept separate from itemshop.lst:
|
||||
// an item listed here is legal only while the matching NPC/shop context is
|
||||
// present on the current EXE request.
|
||||
type currentNPCTradingShop struct {
|
||||
TradeItems map[uint32]currentNPCTradingItem
|
||||
ShopItems map[uint32]struct{}
|
||||
}
|
||||
|
||||
type currentNPCTradingItem struct {
|
||||
BuyGold int64
|
||||
SellGold int64
|
||||
Buyable bool
|
||||
}
|
||||
|
||||
type currentNPCShopPricing struct {
|
||||
Definition dungeonDropItemDefinition
|
||||
BuyGold int64
|
||||
SellGold int64
|
||||
// ZeroGoldDisposal is set only for active-PVF reinforcement/amplification
|
||||
// tickets and portable devices. Many package-issued variants deliberately
|
||||
// omit both [price] and [value]; the NPC may remove those items for zero
|
||||
// gold, while unrelated zero-value goods remain fail-closed.
|
||||
ZeroGoldDisposal bool
|
||||
Buyable bool
|
||||
MaterialExchange bool
|
||||
// RequiresGold 表示这一行 PVF **显式**标了 [price] > 0,也就是"金币"这一半是真的。
|
||||
// 只写 [need material]、不写 [price] 的商品才是纯材料兑换 —— 它们的 [value] 只是
|
||||
// 卖出价基准(同时被当成买入价的兜底),不能据此认定要收金币。
|
||||
// 2026-10-08 现场:3042([price] 2000 + [need material] 3037 x100)被当成纯材料兑换,
|
||||
// 连买 7 次金币一分没扣(日志 gold_after 恒定 49355535)。
|
||||
RequiresGold bool
|
||||
NeedMaterialItem uint32
|
||||
NeedMaterialCount int64
|
||||
TradingShopID uint32
|
||||
}
|
||||
|
||||
func newCurrentNPCShopCatalog(source dnfpvf.Source) (*currentNPCShopCatalog, error) {
|
||||
if source == nil {
|
||||
return nil, errCurrentNPCShopCatalogUnavailable
|
||||
}
|
||||
shopPaths, _, err := loadDungeonDropList(source, currentNPCShopItemListPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errCurrentNPCShopCatalogUnavailable, err)
|
||||
}
|
||||
buyable := make(map[uint32]struct{})
|
||||
var shopItems map[uint32][]uint32
|
||||
shopPathByID := make(map[uint32]string, len(shopPaths))
|
||||
for shopID, listedPath := range shopPaths {
|
||||
shopPathByID[uint32(shopID)] = listedPath
|
||||
}
|
||||
loadedShopCount := 0
|
||||
missingShopCount := 0
|
||||
firstMissingShopPath := ""
|
||||
for shopID, listedPath := range shopPaths {
|
||||
documentPath, text, readErr := readDungeonDropText(source, "itemshop", listedPath)
|
||||
if readErr != nil {
|
||||
if errors.Is(readErr, platformpvf.ErrFileNotFound) || errors.Is(readErr, dnfpvf.ErrDocNotFound) {
|
||||
missingShopCount++
|
||||
if firstMissingShopPath == "" {
|
||||
firstMissingShopPath = listedPath
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("%w: shop=%d path=%s: %v", errCurrentNPCShopCatalogUnavailable, shopID, listedPath, readErr)
|
||||
}
|
||||
document, parseErr := dnfpvf.Parse(documentPath, text)
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("%w: shop=%d path=%s: %v", errCurrentNPCShopCatalogUnavailable, shopID, documentPath, parseErr)
|
||||
}
|
||||
loadedShopCount++
|
||||
// 每个商店的 [item list] 顺序要保留:客户端 op715 的限购计数表可能按
|
||||
// “商品在列表里的槽位下标”而不是道具 id 索引(见 npc_shop_purchase_limit_ack.go)。
|
||||
shopItemOrder := make([]uint32, 0, 32)
|
||||
for _, section := range document.Sections {
|
||||
if strings.ToLower(strings.TrimSpace(section.Name)) != "item list" || section.Start < 0 || section.End > len(document.Tokens) || section.Start > section.End {
|
||||
continue
|
||||
}
|
||||
for _, token := range document.Tokens[section.Start:section.End] {
|
||||
if token.Kind == dnfpvf.TokenInt && token.Int > 0 && token.Int <= math.MaxUint32 {
|
||||
buyable[uint32(token.Int)] = struct{}{}
|
||||
shopItemOrder = append(shopItemOrder, uint32(token.Int))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(shopItemOrder) > 0 {
|
||||
if shopItems == nil {
|
||||
shopItems = make(map[uint32][]uint32, len(shopPaths))
|
||||
}
|
||||
shopItems[uint32(shopID)] = shopItemOrder
|
||||
}
|
||||
}
|
||||
if len(buyable) == 0 {
|
||||
return nil, fmt.Errorf("%w: no [item list] entries", errCurrentNPCShopCatalogUnavailable)
|
||||
}
|
||||
tradingShops, err := loadCurrentNPCTradingShops(source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", errCurrentNPCShopCatalogUnavailable, currentNPCTradingSystemPath, err)
|
||||
}
|
||||
priceTable, err := parseDungeonCardPVFDocument(source, currentNPCShopPriceTablePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", errCurrentNPCShopCatalogUnavailable, currentNPCShopPriceTablePath, err)
|
||||
}
|
||||
rates := priceTable.Ints("rate")
|
||||
if len(rates) < 1 || rates[0] <= 0 || rates[0] > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("%w: %s [rate] is invalid", errCurrentNPCShopCatalogUnavailable, currentNPCShopPriceTablePath)
|
||||
}
|
||||
return ¤tNPCShopCatalog{
|
||||
source: source,
|
||||
buyableItems: buyable,
|
||||
shopItems: shopItems,
|
||||
shopPaths: shopPathByID,
|
||||
tradingShops: tradingShops,
|
||||
equipmentSellRatePermille: rates[0],
|
||||
listedShopCount: len(shopPaths),
|
||||
loadedShopCount: loadedShopCount,
|
||||
missingShopCount: missingShopCount,
|
||||
firstMissingShopPath: firstMissingShopPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) currentNPCShopCatalog() (*currentNPCShopCatalog, error) {
|
||||
if s == nil {
|
||||
return nil, errCurrentNPCShopCatalogUnavailable
|
||||
}
|
||||
s.npcShopCatalogMu.Lock()
|
||||
defer s.npcShopCatalogMu.Unlock()
|
||||
if s.npcShopCatalog != nil || s.npcShopCatalogLoadErr != nil {
|
||||
return s.npcShopCatalog, s.npcShopCatalogLoadErr
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
archive, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err == nil {
|
||||
s.npcShopCatalog, err = newCurrentNPCShopCatalog(archive)
|
||||
}
|
||||
s.npcShopCatalogLoadErr = err
|
||||
if err == nil && s.npcShopCatalog != nil {
|
||||
s.logInfo("dnf NPC shop PVF catalog loaded",
|
||||
"listed_shops", s.npcShopCatalog.listedShopCount,
|
||||
"loaded_shops", s.npcShopCatalog.loadedShopCount,
|
||||
"missing_shops", s.npcShopCatalog.missingShopCount,
|
||||
"buyable_items", len(s.npcShopCatalog.buyableItems),
|
||||
"trading_shops", len(s.npcShopCatalog.tradingShops))
|
||||
if s.npcShopCatalog.missingShopCount > 0 {
|
||||
s.logWarn("dnf NPC shop PVF list contains missing shop files",
|
||||
"missing_shops", s.npcShopCatalog.missingShopCount,
|
||||
"first_missing_shop", s.npcShopCatalog.firstMissingShopPath)
|
||||
}
|
||||
}
|
||||
return s.npcShopCatalog, s.npcShopCatalogLoadErr
|
||||
}
|
||||
|
||||
func loadCurrentNPCTradingShops(source dnfpvf.Source) (map[uint32]currentNPCTradingShop, error) {
|
||||
shops := make(map[uint32]currentNPCTradingShop)
|
||||
if source == nil {
|
||||
return shops, errCurrentNPCShopCatalogUnavailable
|
||||
}
|
||||
text, err := source.ReadText(currentNPCTradingSystemPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, platformpvf.ErrFileNotFound) || errors.Is(err, dnfpvf.ErrDocNotFound) {
|
||||
return shops, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
document, err := dnfpvf.Parse(currentNPCTradingSystemPath, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var currentShopID uint32
|
||||
for _, section := range document.Sections {
|
||||
if section.Start < 0 || section.End > len(document.Tokens) || section.Start > section.End {
|
||||
return nil, fmt.Errorf("invalid section range %q", section.Name)
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSpace(section.Name))
|
||||
tokens := document.Tokens[section.Start:section.End]
|
||||
switch name {
|
||||
case "trade item list":
|
||||
currentShopID = 0
|
||||
for _, token := range tokens {
|
||||
if token.Kind != dnfpvf.TokenInt || token.Int <= 0 || token.Int > math.MaxUint32 {
|
||||
continue
|
||||
}
|
||||
currentShopID = uint32(token.Int)
|
||||
break
|
||||
}
|
||||
if currentShopID == 0 {
|
||||
return nil, errors.New("[trade item list] has no positive shop id")
|
||||
}
|
||||
shop := shops[currentShopID]
|
||||
if shop.TradeItems == nil {
|
||||
shop.TradeItems = make(map[uint32]currentNPCTradingItem)
|
||||
}
|
||||
if shop.ShopItems == nil {
|
||||
shop.ShopItems = make(map[uint32]struct{})
|
||||
}
|
||||
shops[currentShopID] = shop
|
||||
case "/trade item list":
|
||||
currentShopID = 0
|
||||
case "item list":
|
||||
if currentShopID == 0 {
|
||||
continue
|
||||
}
|
||||
rows, parseErr := parseCurrentNPCTradingItemRows(tokens)
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("shop=%d [item list]: %w", currentShopID, parseErr)
|
||||
}
|
||||
shop := shops[currentShopID]
|
||||
for itemID, row := range rows {
|
||||
shop.TradeItems[itemID] = row
|
||||
}
|
||||
shops[currentShopID] = shop
|
||||
case "shop item":
|
||||
if currentShopID == 0 {
|
||||
continue
|
||||
}
|
||||
shop := shops[currentShopID]
|
||||
for _, token := range tokens {
|
||||
if token.Kind == dnfpvf.TokenInt && token.Int > 0 && token.Int <= math.MaxUint32 {
|
||||
shop.ShopItems[uint32(token.Int)] = struct{}{}
|
||||
}
|
||||
}
|
||||
shops[currentShopID] = shop
|
||||
}
|
||||
}
|
||||
return shops, nil
|
||||
}
|
||||
|
||||
func parseCurrentNPCTradingItemRows(tokens []dnfpvf.Token) (map[uint32]currentNPCTradingItem, error) {
|
||||
numbers := make([]float64, 0, len(tokens))
|
||||
integers := make([]bool, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
numbers = append(numbers, float64(token.Int))
|
||||
integers = append(integers, true)
|
||||
case dnfpvf.TokenFloat:
|
||||
numbers = append(numbers, token.Float)
|
||||
integers = append(integers, false)
|
||||
}
|
||||
}
|
||||
if len(numbers)%4 != 0 {
|
||||
return nil, fmt.Errorf("numeric field count=%d is not a multiple of 4", len(numbers))
|
||||
}
|
||||
rows := make(map[uint32]currentNPCTradingItem, len(numbers)/4)
|
||||
for index := 0; index < len(numbers); index += 4 {
|
||||
itemID, sellGold, multiplier, buyFlag := numbers[index], numbers[index+1], numbers[index+2], numbers[index+3]
|
||||
if !integers[index] || !integers[index+1] || !integers[index+3] || itemID <= 0 || itemID > math.MaxUint32 ||
|
||||
sellGold <= 0 || sellGold > math.MaxInt32 || multiplier < 0 || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) ||
|
||||
buyFlag < 0 || buyFlag > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("invalid row item=%v sell=%v multiplier=%v buy_flag=%v", itemID, sellGold, multiplier, buyFlag)
|
||||
}
|
||||
buyGoldValue := sellGold * multiplier
|
||||
if buyGoldValue < 0 || buyGoldValue > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("buy price overflow item=%v price=%v", itemID, buyGoldValue)
|
||||
}
|
||||
rows[uint32(itemID)] = currentNPCTradingItem{
|
||||
BuyGold: int64(buyGoldValue),
|
||||
SellGold: int64(sellGold),
|
||||
Buyable: buyFlag != 0,
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func resolveCurrentNPCShopPricing(shop *currentNPCShopCatalog, items *pvfDungeonDropCatalog, itemID uint32) (currentNPCShopPricing, error) {
|
||||
if shop == nil || shop.source == nil || items == nil || itemID == 0 {
|
||||
return currentNPCShopPricing{}, errCurrentNPCShopCatalogUnavailable
|
||||
}
|
||||
definition, err := items.ResolveItem(itemID)
|
||||
if err != nil {
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d: %v", errCurrentNPCShopProductUnavailable, itemID, err)
|
||||
}
|
||||
document, err := parseDungeonCardPVFDocument(shop.source, definition.PVFPath)
|
||||
if err != nil {
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d path=%s: %v", errCurrentNPCShopProductUnavailable, itemID, definition.PVFPath, err)
|
||||
}
|
||||
price, priceFound := document.Int("price")
|
||||
value, valueFound := document.Int("value")
|
||||
buyGold := int64(0)
|
||||
if priceFound && price >= 0 {
|
||||
buyGold = price
|
||||
} else if valueFound && value >= 0 {
|
||||
buyGold = value
|
||||
}
|
||||
if buyGold > math.MaxInt32 {
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d buy_gold=%d", errCurrentNPCShopProductUnavailable, itemID, buyGold)
|
||||
}
|
||||
|
||||
sellGold := int64(0)
|
||||
switch definition.Kind {
|
||||
case dungeonDropItemEquipment:
|
||||
base := buyGold
|
||||
if valueFound && value >= 0 {
|
||||
base = value
|
||||
}
|
||||
if base > math.MaxInt64/shop.equipmentSellRatePermille {
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d sell price overflow", errCurrentNPCShopProductUnavailable, itemID)
|
||||
}
|
||||
sellGold = base * shop.equipmentSellRatePermille / 1000
|
||||
if sellGold < 1 {
|
||||
sellGold = 1
|
||||
}
|
||||
case dungeonDropItemStackable:
|
||||
if valueFound && value >= 0 {
|
||||
sellGold = value / 5
|
||||
} else if priceFound && price > 0 {
|
||||
sellGold = price / 5
|
||||
}
|
||||
default:
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d kind=%s", errCurrentNPCShopProductUnavailable, itemID, definition.Kind)
|
||||
}
|
||||
if sellGold < 0 || sellGold > math.MaxInt32 {
|
||||
return currentNPCShopPricing{}, fmt.Errorf("%w: item=%d sell_gold=%d", errCurrentNPCShopProductUnavailable, itemID, sellGold)
|
||||
}
|
||||
needMaterial := document.Ints("need material")
|
||||
materialExchange := len(needMaterial) >= 2 && needMaterial[0] > 0 && needMaterial[0] <= math.MaxUint32 && needMaterial[1] > 0
|
||||
_, listed := shop.buyableItems[itemID]
|
||||
pricing := currentNPCShopPricing{
|
||||
Definition: definition,
|
||||
BuyGold: buyGold,
|
||||
SellGold: sellGold,
|
||||
ZeroGoldDisposal: sellGold == 0 && currentNPCShopZeroGoldUpgradeDisposal(document),
|
||||
MaterialExchange: materialExchange,
|
||||
RequiresGold: priceFound && price > 0,
|
||||
}
|
||||
if materialExchange {
|
||||
pricing.NeedMaterialItem = uint32(needMaterial[0])
|
||||
pricing.NeedMaterialCount = needMaterial[1]
|
||||
}
|
||||
// Material-exchange goods pay with their [need material] rows; when the same
|
||||
// PVF block also carries an explicit [price], BOTH are charged (see
|
||||
// RequiresGold and commitCurrentNPCShopMaterialExchange).
|
||||
pricing.Buyable = listed && (materialExchange || buyGold > 0)
|
||||
return pricing, nil
|
||||
}
|
||||
|
||||
func currentNPCShopZeroGoldUpgradeDisposal(document *dnfpvf.Document) bool {
|
||||
if document == nil {
|
||||
return false
|
||||
}
|
||||
if len(document.Ints("equipment reinforcement ticket")) > 0 ||
|
||||
len(document.Ints("equipment amplify reinforcement ticket")) > 0 {
|
||||
return true
|
||||
}
|
||||
action, found := document.Text("action type")
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
action = strings.ToLower(strings.Trim(strings.TrimSpace(action), "` "))
|
||||
return action == "[portable upgrade]" || action == "[portable amplify]"
|
||||
}
|
||||
|
||||
// resolveCurrentNPCShopPricingForContext overlays the context-scoped
|
||||
// etc/tradingsystem.etc rules on top of ordinary itemshop pricing. The time
|
||||
// stone itself has no [price]/[value] in its .stk file, while refined time
|
||||
// stones are intentionally absent from itemshop.lst; both are authorized by
|
||||
// the matching trading NPC row instead.
|
||||
func resolveCurrentNPCShopPricingForContext(shop *currentNPCShopCatalog, items *pvfDungeonDropCatalog, itemID uint32, shopContext uint32, actorContext uint32) (currentNPCShopPricing, error) {
|
||||
pricing, err := resolveCurrentNPCShopPricing(shop, items, itemID)
|
||||
if err != nil {
|
||||
return currentNPCShopPricing{}, err
|
||||
}
|
||||
tradingShopID, tradingShop, found := currentNPCTradingShopForContext(shop, shopContext, actorContext)
|
||||
if !found {
|
||||
return pricing, nil
|
||||
}
|
||||
if row, listed := tradingShop.TradeItems[itemID]; listed {
|
||||
pricing.BuyGold = row.BuyGold
|
||||
pricing.SellGold = row.SellGold
|
||||
pricing.Buyable = row.Buyable && row.BuyGold > 0
|
||||
pricing.MaterialExchange = false
|
||||
pricing.NeedMaterialItem = 0
|
||||
pricing.NeedMaterialCount = 0
|
||||
pricing.RequiresGold = pricing.BuyGold > 0
|
||||
pricing.TradingShopID = tradingShopID
|
||||
return pricing, nil
|
||||
}
|
||||
if _, listed := tradingShop.ShopItems[itemID]; listed {
|
||||
pricing.Buyable = pricing.MaterialExchange || pricing.BuyGold > 0
|
||||
pricing.TradingShopID = tradingShopID
|
||||
}
|
||||
return pricing, nil
|
||||
}
|
||||
|
||||
func currentNPCTradingShopForContext(shop *currentNPCShopCatalog, shopContext uint32, actorContext uint32) (uint32, currentNPCTradingShop, bool) {
|
||||
if shop == nil || len(shop.tradingShops) == 0 {
|
||||
return 0, currentNPCTradingShop{}, false
|
||||
}
|
||||
// Current op21 special-trading requests identify the NPC in actorContext
|
||||
// and carry the 0xFFFFFFFF shop sentinel. op22 sell requests carry the
|
||||
// same actorContext and no shop field. Never accept a trading-system NPC
|
||||
// supplied only through shopContext: doing so lets a forged ordinary-shop
|
||||
// request bypass the actor authorization that opened the special UI.
|
||||
if actorContext == 0 || shopContext != 0 && shopContext != math.MaxUint32 {
|
||||
return 0, currentNPCTradingShop{}, false
|
||||
}
|
||||
if tradingShop, found := shop.tradingShops[actorContext]; found {
|
||||
return actorContext, tradingShop, true
|
||||
}
|
||||
return 0, currentNPCTradingShop{}, false
|
||||
}
|
||||
|
||||
func currentNPCShopContainsItem(catalog *currentNPCShopCatalog, itemID uint32) bool {
|
||||
if catalog == nil {
|
||||
return false
|
||||
}
|
||||
_, found := catalog.buyableItems[itemID]
|
||||
return found
|
||||
}
|
||||
|
||||
// currentNPCShopItemTradingListed reports whether a special-trading NPC row owns
|
||||
// this item's sell price. Time stones and refined time stones carry no
|
||||
// [price]/[value] in their own scripts and are priced by the matching
|
||||
// etc/tradingsystem.etc row instead, so a zero price for one of them means the
|
||||
// request came from a non-matching NPC rather than that the goods are worthless.
|
||||
func currentNPCShopItemTradingListed(catalog *currentNPCShopCatalog, itemID uint32) bool {
|
||||
if catalog == nil || itemID == 0 {
|
||||
return false
|
||||
}
|
||||
for _, tradingShop := range catalog.tradingShops {
|
||||
if _, listed := tradingShop.TradeItems[itemID]; listed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user