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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user