Files
bd2/go/internal/server/world/npc_reputation.go
T

211 lines
5.8 KiB
Go

package world
import (
"bd2server/internal/server/gamedata"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"encoding/json"
"fmt"
"sort"
"time"
)
type npcReputationRuntime struct {
store stateio.Store
load func(int) (gamedata.NPCReputationDesign, error)
inns func(int) ([]gamedata.InnRule, error)
now func() time.Time
}
type npcReputationSnapshot struct {
Starts map[string]int64
Claims map[string]bool
}
func (s *Service) ConfigureNPCRuntime(root, version string, store stateio.Store) error {
if store == nil {
return fmt.Errorf("world: missing NPC reputation store")
}
s.npcReputation = &npcReputationRuntime{store: store, load: func(pack int) (gamedata.NPCReputationDesign, error) {
return gamedata.LoadNPCReputation(root, version, pack)
}, inns: func(pack int) ([]gamedata.InnRule, error) { return gamedata.LoadInns(root, version, pack) }, now: time.Now}
return nil
}
func (r *npcReputationRuntime) snapshot() (npcReputationSnapshot, error) {
v := npcReputationSnapshot{Starts: map[string]int64{}, Claims: map[string]bool{}}
b, err := r.store.Load("npcreputation")
if err != nil {
return v, err
}
if b != nil {
if err = stateio.RequireExactJSONObject(b, "Starts", "Claims"); err != nil {
return v, err
}
if err = json.Unmarshal(b, &v); err != nil || v.Starts == nil || v.Claims == nil {
return v, fmt.Errorf("world: invalid NPC reputation state")
}
}
for _, start := range v.Starts {
if start <= 0 {
return v, fmt.Errorf("world: invalid NPC reputation timestamp")
}
}
return v, nil
}
func reputationKey(pack int, group uint64) string { return fmt.Sprintf("%d/%d", pack, group) }
func (s *Service) reputationState(pack int, rule gamedata.NPCReputationRule) (uint64, uint64, error) {
if s.npcReputation == nil {
return 1, 0, nil
}
v, err := s.npcReputation.snapshot()
if err != nil {
return 0, 0, err
}
start, ok := v.Starts[reputationKey(pack, rule.ID)]
if !ok {
return 1, 0, nil
}
elapsed := s.npcReputation.now().Unix() - start
if elapsed < 0 {
elapsed = 0
}
if uint64(elapsed) >= rule.DownHours*3600 {
return 1, 0, nil
}
return 2, uint64(elapsed), nil
}
func (s *Service) CompleteNPCReputation(identity string, pack int, group uint64) ([]byte, error) {
if s.npcReputation == nil || identity == "" || group == 0 || !s.packUnlocked(pack) {
return nil, ErrInvalidRequest
}
d, err := s.npcReputation.load(pack)
if err != nil {
return nil, err
}
rule, ok := d.Groups[group]
if !ok {
return nil, fmt.Errorf("world: unknown reputation group")
}
v, err := s.npcReputation.snapshot()
if err != nil {
return nil, err
}
claim := fmt.Sprintf("%s:%d:%d", identity, pack, group)
if !v.Claims[claim] {
v.Claims[claim] = true
v.Starts[reputationKey(pack, group)] = s.npcReputation.now().Unix()
b, e := json.Marshal(v)
if e != nil {
return nil, e
}
if e = s.npcReputation.store.Save("npcreputation", b); e != nil {
return nil, e
}
}
state, elapsed, err := s.reputationState(pack, rule)
if err != nil {
return nil, err
}
return reputationWire(group, state, elapsed), nil
}
func reputationWire(group, state, elapsed uint64) []byte {
b := wire.AppendVarint(nil, 1, group)
b = wire.AppendVarint(b, 2, state)
if elapsed > 0 {
b = wire.AppendVarint(b, 3, elapsed)
}
return b
}
func (s *Service) npcReputationRows(pack int) ([][]byte, error) {
if s.npcReputation == nil {
return nil, nil
}
d, err := s.npcReputation.load(pack)
if err != nil {
return nil, err
}
var groups []uint64
for id := range d.Groups {
groups = append(groups, id)
}
sort.Slice(groups, func(i, j int) bool { return groups[i] < groups[j] })
var rows [][]byte
for _, id := range groups {
state, elapsed, e := s.reputationState(pack, d.Groups[id])
if e != nil {
return nil, e
}
rows = append(rows, reputationWire(id, state, elapsed))
}
return rows, nil
}
func (s *Service) NPCShopReputation(pack uint64) (uint64, uint64, error) {
current, err := s.CurrentPackID()
if err != nil || pack == 0 || !s.packUnlocked(int(pack)) || s.npcReputation == nil {
return 0, 0, ErrInvalidRequest
}
d, err := s.npcReputation.load(int(pack))
if err != nil {
return 0, 0, err
}
var group uint64
position, ok := s.state.Position()
if ok && position.PackID == current {
currentDesign := d
if pack != uint64(current) {
currentDesign, err = s.npcReputation.load(current)
if err != nil {
return 0, 0, err
}
}
group = currentDesign.MapGroups[position.Position.MapID]
}
if group == 0 && len(d.Groups) == 1 {
for id := range d.Groups {
group = id
}
}
rule, ok := d.Groups[group]
if !ok {
return 1, 0, nil
}
state, _, err := s.reputationState(int(pack), rule)
if err != nil {
return 0, 0, err
}
if state == 2 {
return state, rule.GoodPrice, nil
}
return state, 0, nil
}
// InnContext validates the motel's real map, rather than trusting an NPC ID
// supplied from another pack or the previously visited scene.
func (s *Service) InnContext(pack, npc uint64) (gamedata.InnRule, uint64, error) {
var empty gamedata.InnRule
current, err := s.CurrentPackID()
if err != nil || s.npcReputation == nil || (pack != 0 && pack != uint64(current)) || !s.packUnlocked(current) {
return empty, 0, ErrInvalidRequest
}
position, ok := s.state.Position()
if !ok || position.PackID != current {
return empty, 0, fmt.Errorf("world: inn requires current field position")
}
rules, err := s.npcReputation.inns(current)
if err != nil {
return empty, 0, err
}
d, err := s.npcReputation.load(current)
if err != nil {
return empty, 0, err
}
for _, r := range rules {
if r.MapID != uint64(position.Position.MapID) || (npc != 0 && npc != r.NPCID) {
continue
}
state, _, e := s.reputationState(current, d.Groups[r.MapGroup])
return r, state, e
}
return empty, 0, fmt.Errorf("world: motel NPC outside current map")
}