fix(server): support dynamic hidden event pack purchase and entry

why the f^^kkkkkk does sh^t AI love writing useless tests so much just to clutter up my repo, looks like I’ll have to find some time to clean this s^^t up later.
This commit is contained in:
2026-10-05 20:37:38 +08:00
parent e15ee4645e
commit 3b08040a0e
13 changed files with 592 additions and 11 deletions
+6
View File
@@ -956,6 +956,12 @@ func serve(args []string) (serveErr error) {
}
eventPlayService.AttachBattleChallenges(eventBattleChallenges)
eventPlayService.AttachHubCalendars(serverConfig)
if err := eventPlayService.AttachFieldBindingsFile(filepath.Join(filepath.Dir(*worldSeed), "event_field_bindings.json")); err != nil {
return fmt.Errorf("attach hidden field bindings: %w", err)
}
if err := worldService.AttachEventFieldPacks(eventPlayService); err != nil {
return fmt.Errorf("attach event field packs: %w", err)
}
battleService.AttachEventBattle(eventPlayService)
eventActionsDesign, err := gamedata.LoadEventActionsDesign(gameData, *gameDataVersion)
if err != nil {
@@ -0,0 +1,52 @@
package eventplay
import (
"bd2server/internal/server/versionconfig"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
)
// FieldBinding records client constants that are not expressed as foreign
// keys in GameData. Concrete pack identities belong in the version seed.
type FieldBinding struct {
PackID int `json:"pack_id"`
EventType uint64 `json:"event_type"`
ContentOpenType uint64 `json:"content_open_type"`
}
func (s *Service) AttachFieldBindingsFile(path string) error {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var seed struct {
Version string `json:"version"`
Bindings []FieldBinding `json:"bindings"`
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err = decoder.Decode(&seed); err != nil {
return err
}
if err = decoder.Decode(new(any)); err != io.EOF {
return fmt.Errorf("eventplay: trailing field binding data")
}
if seed.Version != versionconfig.Current().GameVersion {
return fmt.Errorf("eventplay: field binding version mismatch")
}
s.mu.Lock()
defer s.mu.Unlock()
seen := map[int]bool{}
for _, b := range seed.Bindings {
pack, ok := s.design.FieldPacks[b.PackID]
if !ok || len(pack.MapIDs) == 0 || b.EventType == 0 || seen[b.PackID] {
return fmt.Errorf("eventplay: invalid field binding pack %d", b.PackID)
}
seen[b.PackID] = true
}
s.fieldBindings = append([]FieldBinding(nil), seed.Bindings...)
return nil
}
+155
View File
@@ -0,0 +1,155 @@
package eventplay
import (
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
"fmt"
"sort"
)
// ListEventFieldPacks authorizes hidden fields through installed pack rules
// and the live project calendar. A hub field and a minigame field are separate
// routes: some public hubs have no PackEventMiniGameTable row at all.
func (s *Service) ListEventFieldPacks() ([]gamedata.EventFieldPack, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.eventFieldPacks()
}
func (s *Service) ResolveEventFieldPack(id int) (gamedata.EventFieldPack, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
packs, err := s.eventFieldPacks()
if err != nil {
return gamedata.EventFieldPack{}, false, err
}
for _, p := range packs {
if p.ID == id {
return p, true, nil
}
}
return gamedata.EventFieldPack{}, false, nil
}
func (s *Service) eventFieldPacks() ([]gamedata.EventFieldPack, error) {
now := s.now().UnixMilli()
result := map[int]gamedata.EventFieldPack{}
// Bound minigames use the hub play window even when their global schedule
// has a longer archive window. Zero means a known, currently closed binding.
boundEnds := map[uint64]int64{}
add := func(id int, uid, game, hub, mapID, point uint64, end int64) error {
base, ok := s.design.FieldPacks[id]
if !ok {
return nil
}
if len(base.MapIDs) == 0 {
return fmt.Errorf("eventplay: hidden pack %d has no installed map", id)
}
if mapID != 0 {
found := false
for _, m := range base.MapIDs {
if uint64(m) == mapID {
found = true
}
}
if !found {
return fmt.Errorf("eventplay: pack %d calendar map %d mismatch", id, mapID)
}
}
if old, exists := result[id]; exists && old.End >= end {
return nil
}
base.MapIDs = append([]int(nil), base.MapIDs...)
base.BuyRewards = append([]gamedata.Reward(nil), base.BuyRewards...)
base.ScheduleUID, base.GameID, base.HubID, base.End = uid, game, hub, end
base.InitialMapID, base.PointPositionID = mapID, point
if base.InitialMapID == 0 {
base.InitialMapID = uint64(base.MapIDs[0])
}
result[id] = base
return nil
}
if s.hubCalendars != nil {
_, body, handled, err := s.hubCalendars.Handle("/EventHubInfo", wire.AppendVarint(nil, 1, 1))
if err != nil {
return nil, err
}
if handled {
err = wire.Walk(body, func(f wire.Field) error {
if f.Number != 1 || f.Type != 2 {
return nil
}
start, end := int64(num(f.Value, 3)), int64(num(f.Value, 4))
if err := wire.Walk(f.Value, func(setting wire.Field) error {
if setting.Number != 6 || setting.Type != 2 || num(setting.Value, 2) != 6 {
return nil
}
uids, err := list(setting.Value, 3)
if err != nil {
return err
}
for _, uid := range uids {
if _, exists := boundEnds[uid]; !exists {
boundEnds[uid] = 0
}
if now >= start && now < end && boundEnds[uid] < end {
boundEnds[uid] = end
}
}
return nil
}); err != nil {
return err
}
if now < start || now >= end {
return nil
}
hubID := num(f.Value, 2)
hub, e := s.design.Row("PackEventHubTable", 14, hubID)
if e != nil {
return e
}
return add(int(num(hub, 20)), num(f.Value, 1), 0, hubID, 0, 0, end)
})
if err != nil {
return nil, err
}
}
}
for _, c := range s.registry.List() {
for _, binding := range s.fieldBindings {
if c.Type == binding.EventType && now >= c.Start && now < c.End {
if err := add(binding.PackID, c.UID, c.ID, 0, 0, 0, c.End); err != nil {
return nil, err
}
p := result[binding.PackID]
p.ContentOpenType = binding.ContentOpenType
result[binding.PackID] = p
}
}
if c.Type != 11 || now < c.Start || now >= c.End {
continue
}
end := c.End
if hubEnd, bound := boundEnds[c.UID]; bound {
if hubEnd == 0 {
continue
}
if hubEnd < end {
end = hubEnd
}
}
game, err := s.design.Row("PackEventMiniGameTable", 8, c.ID)
if err != nil {
return nil, err
}
if err = add(int(num(game, 12)), c.UID, c.ID, 0, num(game, 9), num(game, 13), end); err != nil {
return nil, err
}
}
var packs []gamedata.EventFieldPack
for _, p := range result {
packs = append(packs, p)
}
sort.Slice(packs, func(i, j int) bool { return packs[i].ID < packs[j].ID })
return packs, nil
}
@@ -0,0 +1,96 @@
package eventplay
import (
"bd2server/internal/server/events"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/readonly"
"bd2server/internal/server/wire"
"os"
"testing"
"time"
)
func TestClientConstantFieldBindingRequiresActiveMatchingEvent(t *testing.T) {
s, _ := makeService(t)
s.design.FieldPacks = map[int]gamedata.EventFieldPack{734: {ID: 734, MapIDs: []int{735}, InitialPosition: "{}"}}
s.fieldBindings = []FieldBinding{{PackID: 734, EventType: 20, ContentOpenType: 20}}
reg := events.NewRegistry()
if err := reg.Replace([]events.Schedule{{UID: 32, Type: 20, ID: 81, Start: 100, End: 200}}); err != nil {
t.Fatal(err)
}
s.registry = reg
p, ok, err := s.ResolveEventFieldPack(734)
if err != nil || !ok || p.ScheduleUID != 32 || p.ContentOpenType != 20 {
t.Fatalf("%+v %v %v", p, ok, err)
}
s.now = func() time.Time { return time.UnixMilli(200) }
if _, ok, err := s.ResolveEventFieldPack(734); err != nil || ok {
t.Fatalf("expired tactics %v %v", ok, err)
}
}
func TestInstalledTacticsFieldBinding(t *testing.T) {
root := os.Getenv("BD2_REAL_GAMEDATA")
if root == "" {
t.Skip("BD2_REAL_GAMEDATA not configured")
}
reg := events.NewRegistry()
if err := reg.Replace([]events.Schedule{{UID: 81, Type: 20, ID: 8, Start: 1, End: 9999999999999}}); err != nil {
t.Fatal(err)
}
// Use the real catalog but an in-memory account and controlled clock.
d, err := gamedata.LoadEventPlayCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
svc, _ := makeService(t)
svc.design = d
svc.registry = reg
if err = svc.AttachFieldBindingsFile("../../../seed/v2_35_10/event_field_bindings.json"); err != nil {
t.Fatal(err)
}
p, ok, err := svc.ResolveEventFieldPack(20000)
if err != nil || !ok || p.ContentOpenType != 20 || len(p.MapIDs) != 1 || p.MapIDs[0] != 200001 || p.BuyPrice != 0 {
t.Fatalf("%+v %v %v", p, ok, err)
}
}
func TestHiddenHubPackUsesCalendarPlayWindowAndInstalledMaps(t *testing.T) {
s, _ := makeService(t)
s.registry = events.NewRegistry()
s.design.FieldPacks = map[int]gamedata.EventFieldPack{912: gamedata.EventFieldPack{ID: 912, MapIDs: []int{991, 992}, InitialPosition: "{}", BuyType: 4, BuyPrice: 20}}
s.design.Tables["PackEventHubTable"] = [][]byte{wire.AppendVarint(wire.AppendVarint(nil, 14, 55), 20, 912)}
scalar := func(n int, v uint64) readonly.Field { return readonly.Field{Number: n, Varint: v} }
s.AttachHubCalendars(&readonly.Seed{Responses: map[string]readonly.Response{"/EventHubInfo": {PacketCode: 222, Fields: []readonly.Field{{Number: 1, Type: 2, Fields: []readonly.Field{scalar(1, 77), scalar(2, 55), scalar(3, 100), scalar(4, 200), scalar(5, 300)}}}}}})
p, ok, err := s.ResolveEventFieldPack(912)
if err != nil || !ok || p.ScheduleUID != 77 || p.HubID != 55 || p.End != 200 || p.BuyPrice != 20 || p.InitialMapID != 991 || p.InitialPosition != "{}" {
t.Fatalf("pack=%+v ok=%v err=%v", p, ok, err)
}
p.MapIDs[0] = 0
again, _, _ := s.ResolveEventFieldPack(912)
if again.MapIDs[0] != 991 {
t.Fatal("caller changed design maps")
}
s.now = func() time.Time { return time.UnixMilli(200) }
if _, ok, err := s.ResolveEventFieldPack(912); err != nil || ok {
t.Fatalf("post-play purchase ok=%v err=%v", ok, err)
}
}
func TestHiddenMiniGamePackRejectsForeignMapAndInactiveCalendar(t *testing.T) {
s, _ := makeService(t)
s.design.FieldPacks = map[int]gamedata.EventFieldPack{812: {ID: 812, MapIDs: []int{811}}}
s.design.Tables["PackEventMiniGameTable"] = [][]byte{row(map[int]uint64{8: 1, 12: 812, 9: 810, 13: 101})}
if _, _, err := s.ResolveEventFieldPack(812); err == nil {
t.Fatal("foreign map accepted")
}
s.design.Tables["PackEventMiniGameTable"] = [][]byte{row(map[int]uint64{8: 1, 12: 812, 9: 811, 13: 101})}
p, ok, err := s.ResolveEventFieldPack(812)
if err != nil || !ok || p.GameID != 1 || p.PointPositionID != 101 {
t.Fatalf("%+v %v %v", p, ok, err)
}
s.now = func() time.Time { return time.UnixMilli(9999999) }
if _, ok, err := s.ResolveEventFieldPack(812); err != nil || ok {
t.Fatalf("expired game available %v %v", ok, err)
}
}
+1
View File
@@ -59,6 +59,7 @@ type Service struct {
rooms RoomRuntime
onProgress func(uint64, uint64, uint64) error
hubCalendars *readonly.Seed
fieldBindings []FieldBinding
}
func Open(store stateio.Store, root, version string, registry events.Resolver, economy Rewards) (*Service, error) {
@@ -0,0 +1,88 @@
package gamedata
import (
"database/sql"
"fmt"
)
// EventFieldPack combines installed hidden-pack rules with a current calendar
// identity. Calendar times use milliseconds, as do event schedule protocols.
type EventFieldPack struct {
ID int
ScheduleUID, GameID, HubID uint64
MapIDs []int
InitialPosition string
InitialMapID, PointPositionID uint64
BuyType, BuyPrice uint64
ContentOpenType uint64
BuyRewards []Reward
End int64
}
func loadEventFieldPacks(db *sql.DB) (map[int]EventFieldPack, error) {
packs := map[int]EventFieldPack{}
rows, err := db.Query("SELECT id,ProtoBuf FROM PackTable ORDER BY id")
if err != nil {
return nil, err
}
for rows.Next() {
var id int
var raw []byte
if err = rows.Scan(&id, &raw); err != nil {
rows.Close()
return nil, err
}
kind, e := optionalScalar(raw, 55)
if e != nil {
rows.Close()
return nil, e
}
if kind != 100 {
continue
}
protoID, e := optionalScalar(raw, 25)
if e != nil || protoID != uint64(id) || id <= 0 {
rows.Close()
return nil, fmt.Errorf("gamedata: invalid hidden pack %d", id)
}
p := EventFieldPack{ID: id, InitialPosition: "{}"}
if p.BuyType, err = optionalScalar(raw, 11); err != nil {
rows.Close()
return nil, err
}
if p.BuyPrice, err = optionalScalar(raw, 7); err != nil {
rows.Close()
return nil, err
}
rewards, e := eventGameRewards(raw, 10, 9, 8)
if e != nil {
rows.Close()
return nil, e
}
for _, r := range rewards {
p.BuyRewards = append(p.BuyRewards, Reward{Type: r.Type, ID: r.ID, Count: r.Count})
}
packs[id] = p
}
err = rows.Err()
rows.Close()
if err != nil {
return nil, err
}
rows, err = db.Query("SELECT id,packId FROM MapTable ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var id, packID int
if err = rows.Scan(&id, &packID); err != nil {
return nil, err
}
if p, ok := packs[packID]; ok {
p.MapIDs = append(p.MapIDs, id)
packs[packID] = p
}
}
return packs, rows.Err()
}
@@ -0,0 +1,30 @@
package gamedata
import (
"os"
"reflect"
"testing"
)
func TestInstalledHiddenTrackerPackRules(t *testing.T) {
root := os.Getenv("BD2_REAL_GAMEDATA")
if root == "" {
t.Skip("BD2_REAL_GAMEDATA not configured")
}
catalog, err := LoadEventPlayCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
// Current GameData PackTable 12010: hidden type 100, no price/reward
// fields. MapTable binds all three maps, independently of minigame rows.
pack, ok := catalog.FieldPacks[12010]
if !ok || pack.BuyPrice != 0 || pack.BuyType != 0 || len(pack.BuyRewards) != 0 || !reflect.DeepEqual(pack.MapIDs, []int{1201001, 1201002, 1201003}) || pack.InitialPosition != "{}" {
t.Fatalf("tracker pack %+v exists %v", pack, ok)
}
for _, game := range catalog.Tables["PackEventMiniGameTable"] {
id, _ := optionalScalar(game, 12)
if id == 12010 {
t.Fatal("tracker unexpectedly became a PackEventMiniGameTable pack; recheck authoritative mapping")
}
}
}
@@ -8,6 +8,7 @@ import (
// EventPlayCatalog keeps the installed static rows for event gameplay.
type EventPlayCatalog struct {
Tables map[string][][]byte
FieldPacks map[int]EventFieldPack
root, version string
}
@@ -18,6 +19,10 @@ func LoadEventPlayCatalog(root, version string) (*EventPlayCatalog, error) {
}
defer done()
c := &EventPlayCatalog{Tables: map[string][][]byte{}, root: root, version: version}
c.FieldPacks, e = loadEventFieldPacks(db)
if e != nil {
return nil, e
}
names := []string{"PackEventHubTable", "PackEventListTable", "PackEventStoryGroupTable", "PackEventStoryTable", "PackEventBattleGroupTable", "PackEventBattleTable", "PackEventMiniGameTable", "FieldMiniGameRewardTable", "FieldMiniGameSpeedTable", "RhythmGameMusicTable", "RhythmGameGradeTable", "SichuanStageTable", "SichuanEventTable", "SichuanRewardTable", "HopscotchStageTable", "HopscotchRewardTable", "HopscotchDefaultTable", "FieldMiniGameSurvivalTable", "ActionGameDefaultTable", "ActionGameMissionTable", "ActionGameStageTable", "FieldMiniGameCharTable", "FieldMiniGameMapTable", "FieldMiniGameUpgradeTable", "FieldMiniGameUpgradeGroupTable", "FieldMiniGameSkillGroupTable", "FieldMiniGameSkillTable", "FieldMiniGameCharLevelTable", "FieldMiniGameMonsterTable", "FieldMiniGameSurvivalItemTable", "FieldMiniGameSurvivalBoxTable", "MGDRewardTable", "MGDWaveTable", "MGDDefaultTable"}
for _, name := range names {
rows, e := db.Query("SELECT ProtoBuf FROM " + name)
@@ -0,0 +1,100 @@
package world
import (
"fmt"
"slices"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
)
// EventFieldPackSource binds installed hidden packs to the server's current
// calendars. A purchase receipt alone never authorizes an expired event.
type EventFieldPackSource interface {
ResolveEventFieldPack(int) (gamedata.EventFieldPack, bool, error)
ListEventFieldPacks() ([]gamedata.EventFieldPack, error)
}
func (s *Service) AttachEventFieldPacks(source EventFieldPackSource) error {
if source == nil {
return fmt.Errorf("world: nil event field pack source")
}
s.eventFieldPacks = source
return nil
}
func (s *Service) resolveEventFieldPack(id int) (gamedata.EventFieldPack, bool, error) {
if s.eventFieldPacks == nil {
return gamedata.EventFieldPack{}, false, nil
}
return s.eventFieldPacks.ResolveEventFieldPack(id)
}
func (s *Service) eventPackPurchased(id int) bool {
if s.collection == nil {
return false
}
_, found := s.collection.Grant(packPurchaseIdentity(id))
return found
}
func (s *Service) eventPackDBInfo(pack gamedata.EventFieldPack) []byte {
row := wire.AppendVarint(nil, 1, uint64(pack.ID))
if s.eventPackPurchased(pack.ID) {
row = wire.AppendVarint(row, 8, 1)
}
return row
}
func (s *Service) eventPackInfoRows() ([][]byte, error) {
if s.eventFieldPacks == nil {
return nil, nil
}
packs, err := s.eventFieldPacks.ListEventFieldPacks()
if err != nil {
return nil, err
}
var rows [][]byte
for _, pack := range packs {
// PackManager buys a hidden pack only when PackInfo has no row for it.
if s.eventPackPurchased(pack.ID) {
rows = append(rows, s.eventPackDBInfo(pack))
}
}
return rows, nil
}
func (s *Service) enterEventFieldPack(pack gamedata.EventFieldPack) (int, []byte, bool, error) {
if !s.eventPackPurchased(pack.ID) {
return 0, nil, true, fmt.Errorf("%w: event pack %d is not purchased", ErrInvalidRequest, pack.ID)
}
position := pack.InitialPosition
if position == "" {
return 0, nil, true, fmt.Errorf("world: missing event pack initial position")
}
if saved, found := s.state.Position(); found && saved.PackID == pack.ID {
if !slices.Contains(pack.MapIDs, saved.Position.MapID) {
return 0, nil, true, fmt.Errorf("%w: map outside event pack", ErrInvalidRequest)
}
position = saved.RawJSON
}
response := wire.AppendString(nil, 4, position)
// The common callback dereferences HuntingGroundInfo even in hidden packs.
// Use the domain-generated empty/current snapshot; never borrow the outside
// map's monsters or story progress.
var hunting []byte
if s.huntingGround != nil {
var err error
hunting, err = s.huntingGround.EnsureForPack(pack.ID)
if err != nil {
return 0, nil, true, err
}
}
response = wire.AppendBytes(response, 12, hunting)
// Hidden-pack entry must retain the outside field position: the client
// intentionally suppresses SaveUserPosition while playing these packs. The
// persistent active pack also stays outside, so relogin cannot be stranded
// in a hidden scene after its calendar closes.
s.setCurrentPack(pack.ID)
return 5, response, true, nil
}
+3 -1
View File
@@ -33,7 +33,9 @@ func (s *Service) fieldObjectDesign(pack int) (gamedata.FieldObjectDesign, error
}
if _, story := s.packs[pack]; !story {
if _, field := s.fieldPacks[pack]; !field {
return gamedata.FieldObjectDesign{}, fmt.Errorf("%w: unknown field pack", ErrInvalidRequest)
if _, event, err := s.resolveEventFieldPack(pack); err != nil || !event {
return gamedata.FieldObjectDesign{}, fmt.Errorf("%w: unknown field pack", ErrInvalidRequest)
}
}
}
design, err := s.fieldObjectLoader(pack)
+27 -9
View File
@@ -22,6 +22,16 @@ func (s *Service) handlePackBuy(request []byte) (int, []byte, bool, error) {
if err != nil {
return 0, nil, true, err
}
if pack, found, err := s.resolveEventFieldPack(id); err != nil {
return 0, nil, true, err
} else if found {
bundle, err := s.purchasePack(id, pack.BuyType, pack.BuyPrice, pack.BuyRewards, false)
if err != nil {
return 0, nil, true, err
}
response := wire.AppendBytes(nil, 1, s.eventPackDBInfo(pack))
return 6, wire.AppendBytes(response, 2, bundle), true, nil
}
if !s.packUnlocked(id) {
return 0, nil, true, fmt.Errorf("%w: unavailable purchase pack %d", ErrInvalidRequest, id)
}
@@ -101,14 +111,22 @@ func (s *Service) grantPurchaseRewards(identity string, rewards []gamedata.Rewar
}
func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) {
if s.storyCatalog == nil || s.collection == nil || s.wallet == nil || s.inventory == nil {
if s.storyCatalog == nil {
return nil, fmt.Errorf("world: purchase services unavailable")
}
pack, exists := s.storyCatalog.Packs[id]
if !exists {
return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id)
}
buyRewards := pack.BuyRewards
return s.purchasePack(id, pack.BuyType, pack.BuyPrice, pack.BuyRewards, initial)
}
// purchasePack shares the same durable receipt and enclosing account transaction
// across story and calendar-authorized hidden event packs.
func (s *Service) purchasePack(id int, buyType, buyPrice uint64, buyRewards []gamedata.Reward, initial bool) ([]byte, error) {
if s.collection == nil || s.wallet == nil || s.inventory == nil {
return nil, fmt.Errorf("world: purchase services unavailable")
}
identity := packPurchaseIdentity(id)
if _, owned := s.collection.Grant(identity); owned {
return []byte{}, nil
@@ -118,19 +136,19 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) {
return nil, fmt.Errorf("world: unsupported pack purchase reward type %d", reward.Type)
}
}
if !initial && pack.BuyPrice != 0 {
if !initial && buyPrice != 0 {
var err error
switch pack.BuyType {
switch buyType {
case 2:
_, err = s.wallet.SpendJewelryOnce(identity+":price", pack.BuyPrice)
_, err = s.wallet.SpendJewelryOnce(identity+":price", buyPrice)
case 3:
_, err = s.wallet.SpendFreeJewelryOnce(identity+":price", pack.BuyPrice)
_, err = s.wallet.SpendFreeJewelryOnce(identity+":price", buyPrice)
case 4:
_, err = s.wallet.SpendGoldOnce(identity+":price", pack.BuyPrice)
_, err = s.wallet.SpendGoldOnce(identity+":price", buyPrice)
case 12:
_, err = s.wallet.SpendCatalystOnce(identity+":price", pack.BuyPrice)
_, err = s.wallet.SpendCatalystOnce(identity+":price", buyPrice)
default:
return nil, fmt.Errorf("%w: unsupported pack purchase currency%d", ErrInvalidRequest, pack.BuyType)
return nil, fmt.Errorf("%w: unsupported pack purchase currency%d", ErrInvalidRequest, buyType)
}
if err != nil {
return nil, err
+23 -1
View File
@@ -181,6 +181,7 @@ type Service struct {
packJamDesign *gamedata.PackJamDesign
packJamMu sync.Mutex
fieldPacks map[int]gamedata.FieldPack
eventFieldPacks EventFieldPackSource
fieldObjects map[int]gamedata.FieldObjectDesign
fieldObjectLoader func(int) (gamedata.FieldObjectDesign, error)
fieldReset gamedata.FieldResetSchedule
@@ -262,7 +263,15 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
if err != nil || !found || seq == 0 {
return 0, nil, true, errors.New("world: PackInfo missing sequence")
}
return 4, s.accountPackInfo(), true, nil
response := s.accountPackInfo()
rows, err := s.eventPackInfoRows()
if err != nil {
return 0, nil, true, err
}
for _, row := range rows {
response = wire.AppendBytes(response, 1, row)
}
return 4, response, true, nil
case "/CharInfo":
seq, found, err := wire.Varint(request, 1)
if err != nil || !found || seq == 0 {
@@ -344,6 +353,11 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
if err != nil {
return 0, nil, true, err
}
if eventPack, found, err := s.resolveEventFieldPack(pack); err != nil {
return 0, nil, true, err
} else if found {
return s.enterEventFieldPack(eventPack)
}
if !s.packUnlocked(pack) {
return 0, nil, true, fmt.Errorf("%w: unsupported pack %d", ErrInvalidRequest, pack)
}
@@ -441,6 +455,9 @@ func requestQuest(request []byte) (int, int, error) {
}
func (s *Service) questsFor(packID int) (map[int]gamedata.QuestDesign, bool) {
if _, found, err := s.resolveEventFieldPack(packID); err == nil && found {
return map[int]gamedata.QuestDesign{}, true
}
if _, exists := s.fieldPacks[packID]; exists {
return map[int]gamedata.QuestDesign{}, true
}
@@ -453,6 +470,11 @@ func (s *Service) questsFor(packID int) (map[int]gamedata.QuestDesign, bool) {
// packUnlocked uses installed ContentOpen rules and real account tickets.
func (s *Service) packUnlocked(packID int) bool {
if pack, found, err := s.resolveEventFieldPack(packID); err != nil {
return false
} else if found {
return s.eventPackPurchased(pack.ID)
}
if pack, exists := s.fieldPacks[packID]; exists {
return s.fieldPackUnlocked(pack)
}
@@ -0,0 +1,6 @@
{
"version": "2.35.10",
"bindings": [
{"pack_id": 20000, "event_type": 20, "content_open_type": 20}
]
}