172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// NPCShopDesign is the ordinary in-game shop, distinct from CashShopTable.
|
|
type NPCShopDesign struct {
|
|
Shops map[uint64]NPCShop
|
|
Products map[uint64]map[uint64]NPCProduct
|
|
Sell map[uint64]NPCProduct
|
|
ShopNPCs map[uint64][]uint64
|
|
}
|
|
type NPCShop struct{ ID, PackID, ResetType, ResetCount, StartDay uint64 }
|
|
type NPCProduct struct {
|
|
ID, GroupID, MaxCount, Discount, Premium, NoBargain, Reputation uint64
|
|
Reward, Price Reward
|
|
PremiumPriceType, HighPremium, HighDay, HighShop uint64
|
|
}
|
|
|
|
// ShopInfo.GetShopSellProducts reads only Food and Resource inventory rows.
|
|
// SellItemTable also has equipment prices; those definitions do not create
|
|
// an ordinary ShopSell equipment entry point in the current client.
|
|
func (p NPCProduct) InventorySellable() bool {
|
|
return p.Reward.Type == 5 || p.Reward.Type == 8
|
|
}
|
|
|
|
func LoadNPCShopDesign(root, version string) (NPCShopDesign, error) {
|
|
db, closeDB, err := openStatDatabase(root, version)
|
|
if err != nil {
|
|
return NPCShopDesign{}, err
|
|
}
|
|
defer closeDB()
|
|
d, e := loadNPCShopDesign(db)
|
|
if e != nil {
|
|
return d, e
|
|
}
|
|
d.ShopNPCs = map[uint64][]uint64{}
|
|
packs := map[uint64]bool{}
|
|
for _, shop := range d.Shops {
|
|
packs[shop.PackID] = true
|
|
}
|
|
ids := []uint64{}
|
|
for pack := range packs {
|
|
ids = append(ids, pack)
|
|
}
|
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
|
for _, pack := range ids {
|
|
bindings, e := LoadNPCShopActors(root, version, int(pack))
|
|
if e != nil {
|
|
return d, e
|
|
}
|
|
for shop, actors := range bindings {
|
|
if def, ok := d.Shops[shop]; ok && def.PackID == pack {
|
|
d.ShopNPCs[shop] = actors
|
|
}
|
|
}
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// NPCInfo pairs FieldNpcTable.InteractionList (9) and InteractionValue (12).
|
|
// EInteractionType.Shop=3 identifies the real shop ID, rather than deriving an
|
|
// NPC identity from shop numbering or using one global bargaining discount.
|
|
func LoadNPCShopActors(root, version string, pack int) (map[uint64][]uint64, error) {
|
|
db, done, e := openPackDatabase(root, version, pack)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer done()
|
|
rows, e := db.Query("SELECT id,ProtoBuf FROM FieldNpcTable ORDER BY id")
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer rows.Close()
|
|
out := map[uint64][]uint64{}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if e = rows.Scan(&id, &raw); e != nil {
|
|
return nil, e
|
|
}
|
|
ts, e := packedInts(raw, 9)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
vs, e := packedInts(raw, 12)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
for i, t := range ts {
|
|
if t != 3 {
|
|
continue
|
|
}
|
|
if i >= len(vs) || vs[i] == 0 {
|
|
return nil, fmt.Errorf("gamedata: invalid NPC shop interaction")
|
|
}
|
|
out[vs[i]] = append(out[vs[i]], id)
|
|
}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
func loadNPCShopDesign(db *sql.DB) (NPCShopDesign, error) {
|
|
d := NPCShopDesign{Shops: map[uint64]NPCShop{}, Products: map[uint64]map[uint64]NPCProduct{}, Sell: map[uint64]NPCProduct{}}
|
|
err := friendshipRows(db, "SELECT id,ProtoBuf FROM ShopTable ORDER BY id", func(id uint64, p []byte) error {
|
|
r := NPCShop{}
|
|
for i, dst := range []*uint64{&r.ID, &r.PackID, &r.ResetType, &r.ResetCount, &r.StartDay} {
|
|
v, e := friendshipScalar(p, []int{5, 8, 12, 11, 14}[i])
|
|
if e != nil {
|
|
return e
|
|
}
|
|
*dst = v
|
|
}
|
|
if r.ID != id || id == 0 || r.ResetType > 3 {
|
|
return fmt.Errorf("gamedata: invalid shop %d", id)
|
|
}
|
|
d.Shops[id] = r
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
err = friendshipRows(db, "SELECT id,ProtoBuf FROM ProductTable ORDER BY groupId,id", func(id uint64, p []byte) error {
|
|
r := NPCProduct{}
|
|
for i, dst := range []*uint64{&r.ID, &r.GroupID, &r.MaxCount, &r.Discount, &r.Premium, &r.NoBargain, &r.Reputation, &r.Reward.Type, &r.Reward.ID, &r.Reward.Count, &r.Price.Type, &r.Price.ID, &r.Price.Count} {
|
|
v, e := friendshipScalar(p, []int{7, 6, 1, 2, 9, 8, 13, 5, 4, 3, 12, 11, 10}[i])
|
|
if e != nil {
|
|
return e
|
|
}
|
|
*dst = v
|
|
}
|
|
if r.ID != id || r.Reward.Count == 0 || r.Price.Count == 0 || r.Discount > 100 {
|
|
return fmt.Errorf("gamedata: invalid shop product %d", id)
|
|
}
|
|
if _, ok := d.Shops[r.GroupID]; !ok {
|
|
return fmt.Errorf("gamedata: product missing shop %d", r.GroupID)
|
|
}
|
|
if d.Products[r.GroupID] == nil {
|
|
d.Products[r.GroupID] = map[uint64]NPCProduct{}
|
|
}
|
|
d.Products[r.GroupID][id] = r
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
sellKeys := map[[2]uint64]bool{}
|
|
err = friendshipRows(db, "SELECT id,ProtoBuf FROM SellItemTable ORDER BY id", func(id uint64, p []byte) error {
|
|
r := NPCProduct{}
|
|
for i, dst := range []*uint64{&r.ID, &r.Discount, &r.Premium, &r.Reward.Type, &r.Reward.ID, &r.Reward.Count, &r.Price.Type, &r.Price.ID, &r.Price.Count, &r.PremiumPriceType, &r.HighPremium, &r.HighDay, &r.HighShop} {
|
|
v, e := friendshipScalar(p, []int{8, 1, 10, 4, 3, 2, 13, 12, 11, 9, 6, 5, 7}[i])
|
|
if e != nil {
|
|
return e
|
|
}
|
|
*dst = v
|
|
}
|
|
if id == 0 || r.ID != id || r.Reward.ID == 0 || r.Reward.Count != 1 || r.Price.Type == 0 || r.Price.Count == 0 || r.Discount > 100 {
|
|
return fmt.Errorf("gamedata: invalid sell product %d", id)
|
|
}
|
|
k := [2]uint64{r.Reward.Type, r.Reward.ID}
|
|
if sellKeys[k] {
|
|
return fmt.Errorf("gamedata: ambiguous sell definition %d:%d", k[0], k[1])
|
|
}
|
|
sellKeys[k] = true
|
|
d.Sell[id] = r
|
|
return nil
|
|
})
|
|
return d, err
|
|
}
|