102 lines
3.0 KiB
Go
102 lines
3.0 KiB
Go
package world
|
|
|
|
import (
|
|
"bd2server/internal/server/domain/command"
|
|
"bd2server/internal/server/protocol/wire"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
)
|
|
|
|
func (s *Service) CompleteNPCReputation(ctx command.Context, identity string, pack int, group uint64) ([]byte, error) {
|
|
if s.npcReputation == nil || identity == "" || group == 0 || !s.packUnlocked(ctx, 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(ctx)
|
|
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(ctx.State, "npcreputation", b); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
state, elapsed, err := s.reputationState(ctx, 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(ctx command.Context, 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)
|
|
}
|
|
slices.Sort(groups)
|
|
var rows [][]byte
|
|
for _, id := range groups {
|
|
state, elapsed, e := s.reputationState(ctx, pack, d.Groups[id])
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
rows = append(rows, reputationWire(id, state, elapsed))
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// handleQuestUpdate accepts the existing task update packet used when an NPC
|
|
// conversation finishes. Ordinary Talk is local and sends AchievementUpdate,
|
|
// not a separate NPC dialog packet. QuestUpdate has no NPC identity to validate.
|
|
func (s *Service) handleQuestUpdate(ctx command.Context, request []byte) (int, []byte, bool, error) {
|
|
quest, pack, err := requestQuest(request)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
current, err := s.CurrentPackID(ctx)
|
|
if err != nil || current != pack || !s.canClear(ctx, pack, quest) {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
if s.state.QuestCleared(quest, pack, s.questDifficultyFor(pack, quest)) {
|
|
// A delayed replay must not reinsert a cleared quest into active progress.
|
|
return 19, wire.AppendBytes(wire.AppendVarint(nil, 1, uint64(quest)), 2, nil), true, nil
|
|
}
|
|
if _, err := s.state.UpdateQuest(ctx, request); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
// field 2 is RewardDBInfoBundle, not QuestDBInfo. Task completion/claims
|
|
// remain in QuestClear; an update alone must not invent or duplicate rewards.
|
|
return 19, wire.AppendBytes(wire.AppendVarint(nil, 1, uint64(quest)), 2, nil), true, nil
|
|
}
|