Files
bd2/go/internal/server/missions/user_level_reward.go
T
Flechazo d04791dd31 feat(server): add colored logs, level rewards, hunting dispatch and fiend hunt
Persist claims, AP, teams and presets with session receipts and atomic request rollback. Read rewards and growth from GameData; replace fixed Fiend Hunt user state and preserve the account initialization contract.
2026-10-04 21:27:19 +08:00

149 lines
3.8 KiB
Go

package missions
import (
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/versionconfig"
"bd2server/internal/server/wire"
"encoding/json"
"fmt"
)
type levelRewardSnapshot struct {
Version string `json:"version"`
LevelReward uint64 `json:"level_reward"`
}
// AttachUserLevelRewards reads the dedicated claim bucket in the same store as missions.
func (s *Service) AttachUserLevelRewards(d *gamedata.AchievementLevelDesign) error {
if err := d.Validate(); err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
entries, ok := s.storage.(stateio.EntryStore)
if !ok {
return fmt.Errorf("missions: user level rewards require entry storage")
}
raw, _, err := entries.LoadEntry("missions", "user_level_rewards", "state")
if err != nil {
return err
}
var state levelRewardSnapshot
if raw != nil {
if err := stateio.RequireExactJSONObject(raw, "version", "level_reward"); err != nil {
return err
}
if err := json.Unmarshal(raw, &state); err != nil {
return err
}
if state.Version != versionconfig.State() {
return fmt.Errorf("missions: incompatible user level reward version")
}
if state.LevelReward != 0 {
found := false
for _, l := range d.Levels {
found = found || l.ID == state.LevelReward
}
if !found {
return fmt.Errorf("missions: claimed user level missing from GameData")
}
}
}
s.levelRewards = d
s.levelReward = state.LevelReward
return nil
}
func (s *Service) LevelRewardCount() (uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.levelRewards == nil {
return 0, fmt.Errorf("missions: user level rewards unavailable")
}
return s.levelReward, nil
}
func (s *Service) userLevelReward(request []byte) ([]byte, error) {
if s.levelRewards == nil {
return nil, fmt.Errorf("missions: user level rewards unavailable")
}
if err := requireSeq(request); err != nil {
return nil, err
}
ids, err := packed(request, 2)
if err != nil || len(ids) == 0 {
return nil, ErrInvalidRequest
}
exp, err := s.achievementExperienceLocked()
if err != nil {
return nil, err
}
level := s.levelRewards.Level(exp)
next := s.levelReward
var fresh []gamedata.AchievementLevel
var previous uint64
for _, id := range ids {
if id == 0 || id <= previous {
return nil, ErrInvalidRequest
}
previous = id
index := -1
for i, l := range s.levelRewards.Levels {
if l.ID == id {
index = i
break
}
}
if index < 0 || id > level {
return nil, ErrInvalidRequest
}
if id <= s.levelReward {
continue
}
expected := uint64(0)
for _, l := range s.levelRewards.Levels {
if l.ID > next {
expected = l.ID
break
}
}
if id != expected {
return nil, fmt.Errorf("%w: user level reward requires preceding claims", ErrInvalidRequest)
}
fresh = append(fresh, s.levelRewards.Levels[index])
next = id
}
var items []player.Item
var currencies []gamedata.Reward
for _, l := range fresh {
granted, err := s.grantRewards(fmt.Sprintf("user-level:%d", l.ID), l.Rewards)
if err != nil {
return nil, err
}
items = append(items, granted...)
for _, r := range l.Rewards {
if r.Type == 2 || r.Type == 3 || r.Type == 4 || r.Type == 12 || r.Type == 20 {
currencies = append(currencies, r)
}
}
}
if next != s.levelReward {
raw, err := json.Marshal(levelRewardSnapshot{versionconfig.State(), next})
if err != nil {
return nil, err
}
if err := s.storage.(stateio.EntryStore).PutEntry("missions", "user_level_rewards", "state", raw); err != nil {
return nil, err
}
s.levelReward = next
}
bundle := rewardBundle(items)
for _, r := range currencies {
item := wire.AppendVarint(nil, 3, r.Type)
item = wire.AppendVarint(item, 4, r.Count)
bundle = wire.AppendBytes(bundle, 1, item)
}
return wire.AppendBytes(nil, 1, bundle), nil
}