fix(all): route activity hubs by explicit calendars and clear stale mini event UI
This commit is contained in:
@@ -59,6 +59,9 @@ func (s *Set) ValidateDesign(root, version string) error {
|
||||
return fmt.Errorf("calendar: hub %d: %w", h.HubID, e)
|
||||
}
|
||||
}
|
||||
if err := s.validateMiniHubBindings(play); err != nil {
|
||||
return err
|
||||
}
|
||||
uid := map[uint64]events.Schedule{}
|
||||
for _, v := range s.Events {
|
||||
if v.UID != 0 {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bd2server/internal/server/events"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
// validateMiniHubBindings rejects invalid published routes at startup instead
|
||||
// of letting a client open an incompatible prefab or use another activity UID.
|
||||
func (s *Set) validateMiniHubBindings(design *gamedata.EventPlayCatalog) error {
|
||||
uid := make(map[uint64]events.Schedule)
|
||||
for _, v := range s.Events {
|
||||
if v.UID != 0 {
|
||||
uid[v.UID] = v
|
||||
}
|
||||
}
|
||||
value := func(row []byte, field int) uint64 { v, _, _ := wire.Varint(row, field); return v }
|
||||
for _, hub := range s.EventHubs {
|
||||
table, err := design.Row("PackEventHubTable", 14, hub.HubID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value(table, 13) != 1 {
|
||||
continue
|
||||
}
|
||||
if hub.UID == 0 {
|
||||
return fmt.Errorf("calendar: mini hub %d requires a nonzero UID", hub.HubID)
|
||||
}
|
||||
start, err := timestamp(hub.Start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playEnd, err := timestamp(hub.PlayEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end, err := timestamp(hub.End)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, binding := range hub.Settings {
|
||||
var slot []byte
|
||||
for _, row := range design.Rows("PackEventListTable", 6, hub.HubID) {
|
||||
if value(row, 11) != binding.Slot {
|
||||
continue
|
||||
}
|
||||
if slot != nil {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d is ambiguous in GameData", hub.HubID, binding.Slot)
|
||||
}
|
||||
slot = row
|
||||
}
|
||||
if slot == nil {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d missing GameData", hub.HubID, binding.Slot)
|
||||
}
|
||||
contentType, contentID := value(slot, 9), value(slot, 7)
|
||||
if binding.ProgressType != contentType {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d content type %d, want %d", hub.HubID, binding.Slot, binding.ProgressType, contentType)
|
||||
}
|
||||
eventType, supported := gamedata.MiniHubEventType(contentType)
|
||||
if !supported {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d content type %d has no supported scheduled route", hub.HubID, binding.Slot, contentType)
|
||||
}
|
||||
endType := value(slot, 4)
|
||||
if endType > 1 {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d unsupported end type %d", hub.HubID, binding.Slot, endType)
|
||||
}
|
||||
matches := 0
|
||||
for _, id := range binding.EventUIDs {
|
||||
child, ok := uid[id]
|
||||
if !ok || child.Type != eventType || child.ID != contentID || child.SubID != 0 {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d references incompatible event UID %d", hub.HubID, binding.Slot, id)
|
||||
}
|
||||
childStart, childEnd := child.Start, child.End
|
||||
if childStart < int64(start) {
|
||||
childStart = int64(start)
|
||||
}
|
||||
if childEnd > int64(end) {
|
||||
childEnd = int64(end)
|
||||
}
|
||||
// Project policy: EndDateType=0 closes the slot at PlayEnd;
|
||||
// EndDateType=1 allows it through the final hub End window.
|
||||
if endType == 0 && childEnd > int64(playEnd) {
|
||||
childEnd = int64(playEnd)
|
||||
}
|
||||
if childStart < childEnd {
|
||||
matches++
|
||||
}
|
||||
}
|
||||
if matches != 1 {
|
||||
return fmt.Errorf("calendar: mini hub %d slot %d has %d schedules in its window, want one", hub.HubID, binding.Slot, matches)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bd2server/internal/server/events"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestMiniHubPublishedRoutesValidateIndependentEnumsAndWindows(t *testing.T) {
|
||||
row := func(fields ...uint64) []byte {
|
||||
var b []byte
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
b = wire.AppendVarint(b, int(fields[i]), fields[i+1])
|
||||
}
|
||||
return b
|
||||
}
|
||||
start := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
format := func(d time.Duration) string { return start.Add(d).Format(time.RFC3339) }
|
||||
design := &gamedata.EventPlayCatalog{Tables: map[string][][]byte{
|
||||
"PackEventHubTable": {row(14, 7, 13, 1)},
|
||||
"PackEventListTable": {row(6, 7, 10, 42, 11, 5, 9, 12, 7, 99, 4, 1)},
|
||||
}}
|
||||
fresh := func() *Set {
|
||||
return &Set{
|
||||
Events: []events.Schedule{{UID: 21, Type: 19, ID: 99, Start: start.UnixMilli(), End: start.Add(48 * time.Hour).UnixMilli()}},
|
||||
EventHubs: []EventHub{{UID: 1, HubID: 7, Start: format(0), PlayEnd: format(24 * time.Hour), End: format(48 * time.Hour), Settings: []HubSetting{{Slot: 5, ProgressType: 12, EventUIDs: []uint64{21}}}}},
|
||||
}
|
||||
}
|
||||
if err := fresh().validateMiniHubBindings(design); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
change func(*Set)
|
||||
}{
|
||||
{"wrong content enum", func(s *Set) { s.EventHubs[0].Settings[0].ProgressType = 19 }},
|
||||
{"table id is not slot index", func(s *Set) { s.EventHubs[0].Settings[0].Slot = 42 }},
|
||||
{"missing UID", func(s *Set) { s.EventHubs[0].Settings[0].EventUIDs = []uint64{22} }},
|
||||
{"wrong event enum", func(s *Set) { s.Events[0].Type = 12 }},
|
||||
{"wrong design id", func(s *Set) { s.Events[0].ID = 7 }},
|
||||
{"wrong sub identity", func(s *Set) { s.Events[0].SubID = 7 }},
|
||||
{"no window overlap", func(s *Set) {
|
||||
s.Events[0].Start = start.Add(72 * time.Hour).UnixMilli()
|
||||
s.Events[0].End = start.Add(96 * time.Hour).UnixMilli()
|
||||
}},
|
||||
{"ambiguous schedule", func(s *Set) {
|
||||
other := s.Events[0]
|
||||
other.UID = 22
|
||||
s.Events = append(s.Events, other)
|
||||
s.EventHubs[0].Settings[0].EventUIDs = append(s.EventHubs[0].Settings[0].EventUIDs, 22)
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := fresh()
|
||||
test.change(s)
|
||||
if err := s.validateMiniHubBindings(design); err == nil {
|
||||
t.Fatal("invalid published route accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
// A future hub must validate without comparing its dates to the current clock.
|
||||
design.Tables["PackEventListTable"][0] = row(6, 7, 10, 42, 11, 5, 9, 13, 7, 99, 4, 1)
|
||||
s := fresh()
|
||||
s.EventHubs[0].Settings[0].ProgressType = 13
|
||||
if err := s.validateMiniHubBindings(design); err == nil {
|
||||
t.Fatal("quiz borrowed global bingo event type")
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,8 @@ type CashProduct struct {
|
||||
EventIndex uint64
|
||||
}
|
||||
type HubSetting struct {
|
||||
// Mini hubs bind PackEventListTable.SlotIndex and HubContentType here.
|
||||
// The response's slot ID is the separate static table Id, never SlotIndex.
|
||||
Slot uint64
|
||||
ProgressType uint64
|
||||
EventUIDs []uint64
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eventplay
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/events"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
func TestProjectHubCalendarRetainsPlayEndAndMultipleUIDs(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{wire.AppendVarint(nil, 14, 57)}
|
||||
scalar := func(n int, v uint64) readonly.Field {
|
||||
return readonly.Field{Number: n, Type: 0, Varint: v}
|
||||
}
|
||||
@@ -20,7 +22,7 @@ func TestProjectHubCalendarRetainsPlayEndAndMultipleUIDs(t *testing.T) {
|
||||
}},
|
||||
}}
|
||||
seed := &readonly.Seed{Responses: map[string]readonly.Response{
|
||||
"/EventHubInfo": {Fields: []readonly.Field{hub}},
|
||||
"/EventHubInfo": {PacketCode: 222, Fields: []readonly.Field{hub}},
|
||||
"/MiniGameHubInfo": {PacketCode: 390, Fields: []readonly.Field{
|
||||
{Number: 1, Type: 2, Fields: []readonly.Field{scalar(1, 4), scalar(2, 9), scalar(3, 0)}},
|
||||
}},
|
||||
@@ -51,3 +53,43 @@ func TestProjectHubCalendarRetainsPlayEndAndMultipleUIDs(t *testing.T) {
|
||||
t.Fatal("public calendars persisted as player replies")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniEventHubExcludesOrdinaryPacksAndGameSchedules(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
registry := events.NewRegistry()
|
||||
if err := registry.Replace([]events.Schedule{
|
||||
{UID: 1, Type: 8, ID: 20, Start: 1, End: 9999999},
|
||||
{UID: 2, Type: 11, ID: 41, Start: 1, End: 9999999},
|
||||
{UID: 3, Type: 8, ID: 1003, Start: 1, End: 9999999},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.registry = registry
|
||||
normal := wire.AppendVarint(nil, 14, 20)
|
||||
normal = wire.AppendVarint(normal, 20, 20)
|
||||
mini := wire.AppendVarint(nil, 14, 1003)
|
||||
mini = wire.AppendVarint(mini, 20, 1003)
|
||||
mini = wire.AppendVarint(mini, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{normal, mini}
|
||||
s.AttachHubCalendars(&readonly.Seed{Responses: map[string]readonly.Response{"/EventHubInfo": {Fields: []readonly.Field{
|
||||
{Number: 1, Type: 2, Fields: []readonly.Field{{Number: 1, Type: 0, Varint: 1}, {Number: 2, Type: 0, Varint: 20}, {Number: 3, Type: 0, Varint: 1}, {Number: 4, Type: 0, Varint: 9999999}, {Number: 5, Type: 0, Varint: 9999999}}},
|
||||
{Number: 1, Type: 2, Fields: []readonly.Field{{Number: 1, Type: 0, Varint: 3}, {Number: 2, Type: 0, Varint: 1003}, {Number: 3, Type: 0, Varint: 1}, {Number: 4, Type: 0, Varint: 9999999}, {Number: 5, Type: 0, Varint: 9999999}}},
|
||||
}}}})
|
||||
|
||||
code, response, handled, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil || !handled || code != 534 {
|
||||
t.Fatalf("mini hub response code=%d handled=%t err=%v", code, handled, err)
|
||||
}
|
||||
count := 0
|
||||
if err := wire.Walk(response, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
count++
|
||||
if num(f.Value, 1) != 3 || num(f.Value, 2) != 1003 {
|
||||
t.Fatalf("ordinary pack or game exposed as MiniEventMainUI: %x", f.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil || count != 1 {
|
||||
t.Fatalf("mini hubs=%d err=%v", count, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package eventplay
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/calendar"
|
||||
"bd2server/internal/server/events"
|
||||
"bd2server/internal/server/readonly"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestInstalledHubCalendarsNeverRequestTheWrongPrefabKind(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
t.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
set, err := calendar.LoadDirectory("../../../../schedules", "2.35.10", "20260923193640")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := set.ValidateDesign(root, "20260923193640"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registry := events.NewRegistry()
|
||||
if err := registry.Replace(set.Events); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := stateio.NewMemory()
|
||||
s, err := Open(store, root, "20260923193640", registry, &econ{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed, err := set.ApplyReadonly(&readonly.Seed{Version: "2.35.10", Responses: map[string]readonly.Response{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.AttachHubCalendars(seed)
|
||||
for _, route := range []struct {
|
||||
path string
|
||||
kind uint64
|
||||
}{{"/EventHubInfo", 0}, {"/MiniEventHubInfo", 1}} {
|
||||
_, body, _, err := s.HandleSession(route.path, wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil {
|
||||
t.Fatal(route.path, err)
|
||||
}
|
||||
got := map[uint64]bool{}
|
||||
if err := wire.Walk(body, func(f wire.Field) error {
|
||||
if f.Number != 1 || f.Type != 2 {
|
||||
return nil
|
||||
}
|
||||
hubID := num(f.Value, 2)
|
||||
row, err := s.design.Row("PackEventHubTable", 14, hubID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if num(row, 13) != route.kind {
|
||||
t.Fatalf("%s exposes incompatible HubType for hub %d", route.path, hubID)
|
||||
}
|
||||
got[num(f.Value, 1)] = true
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, hub := range set.EventHubs {
|
||||
row, err := s.design.Row("PackEventHubTable", 14, hub.HubID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got[hub.UID] != (num(row, 13) == route.kind) {
|
||||
t.Fatal("published hub lost or appeared in both routes", route.path, hub.HubID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if saved, err := store.Load("eventplay"); err != nil || len(saved) != 0 {
|
||||
t.Fatal("public calendars persisted player state", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package eventplay
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/events"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/wire"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// miniHubSchedule is a project calendar identity; HubID is distinct from the
|
||||
// pack ID used by EventPack schedules.
|
||||
type miniHubBinding struct {
|
||||
Slot, ContentType uint64
|
||||
UIDs []uint64
|
||||
}
|
||||
|
||||
type miniHubSchedule struct {
|
||||
UID, HubID uint64
|
||||
Start, PlayEnd, End int64
|
||||
Bindings []miniHubBinding
|
||||
}
|
||||
|
||||
func (s *Service) miniEventHubs(req []byte) ([]byte, error) {
|
||||
var hubs []miniHubSchedule
|
||||
if s.hubCalendars == nil {
|
||||
return nil, nil
|
||||
}
|
||||
_, body, handled, err := s.hubCalendars.Handle("/EventHubInfo", req)
|
||||
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
|
||||
}
|
||||
hub := miniHubSchedule{UID: num(f.Value, 1), HubID: num(f.Value, 2), Start: int64(num(f.Value, 3)), PlayEnd: int64(num(f.Value, 4)), End: int64(num(f.Value, 5))}
|
||||
if err := wire.Walk(f.Value, func(setting wire.Field) error {
|
||||
if setting.Number != 6 || setting.Type != 2 {
|
||||
return nil
|
||||
}
|
||||
uids, err := list(setting.Value, 3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.Bindings = append(hub.Bindings, miniHubBinding{num(setting.Value, 1), num(setting.Value, 2), uids})
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
hubs = append(hubs, hub)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
sort.Slice(hubs, func(i, j int) bool { return hubs[i].UID < hubs[j].UID })
|
||||
rows := s.registry.List()
|
||||
var out []byte
|
||||
for _, hub := range hubs {
|
||||
table, err := s.design.Row("PackEventHubTable", 14, hub.HubID)
|
||||
if err != nil || num(table, 13) != 1 {
|
||||
continue
|
||||
}
|
||||
v := wire.AppendVarint(nil, 1, hub.UID)
|
||||
v = wire.AppendVarint(v, 2, hub.HubID)
|
||||
v = wire.AppendVarint(v, 3, uint64(hub.Start))
|
||||
v = wire.AppendVarint(v, 4, uint64(hub.PlayEnd))
|
||||
v = wire.AppendVarint(v, 5, uint64(hub.End))
|
||||
slots := s.design.Rows("PackEventListTable", 6, hub.HubID)
|
||||
sort.Slice(slots, func(i, j int) bool { return num(slots[i], 10) < num(slots[j], 10) })
|
||||
for _, binding := range hub.Bindings {
|
||||
found := false
|
||||
for _, slot := range slots {
|
||||
if binding.Slot == num(slot, 11) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("eventplay: mini hub %d static slot index %d missing", hub.UID, binding.Slot)
|
||||
}
|
||||
}
|
||||
for _, slot := range slots {
|
||||
contentType, contentID := num(slot, 9), num(slot, 7)
|
||||
bound := map[uint64]bool{}
|
||||
for _, binding := range hub.Bindings {
|
||||
if binding.Slot == num(slot, 11) {
|
||||
if binding.ContentType != contentType {
|
||||
return nil, fmt.Errorf("eventplay: mini hub %d slot %d content type mismatch", hub.UID, num(slot, 10))
|
||||
}
|
||||
for _, uid := range binding.UIDs {
|
||||
bound[uid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(bound) == 0 {
|
||||
continue
|
||||
}
|
||||
eventType, subID, known := s.miniSlotSchedule(contentType, contentID)
|
||||
if !known {
|
||||
return nil, fmt.Errorf("eventplay: mini hub %d slot %d content type %d unsupported", hub.UID, num(slot, 10), contentType)
|
||||
}
|
||||
for uid := range bound {
|
||||
found := false
|
||||
for _, row := range rows {
|
||||
if row.UID == uid && row.Type == eventType && row.ID == contentID && row.SubID == subID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("eventplay: mini hub %d slot %d schedule %d identity mismatch", hub.UID, num(slot, 10), uid)
|
||||
}
|
||||
}
|
||||
var matches []events.Schedule
|
||||
for _, row := range rows {
|
||||
if bound[row.UID] && row.Type == eventType && row.ID == contentID && row.SubID == subID && row.UID != 0 && row.Start < hub.End && hub.Start < row.End {
|
||||
matches = append(matches, row)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("eventplay: mini hub %d slot %d has ambiguous schedules", hub.UID, num(slot, 10))
|
||||
}
|
||||
child := matches[0]
|
||||
start, end := child.Start, child.End
|
||||
if start < hub.Start {
|
||||
start = hub.Start
|
||||
}
|
||||
if end > hub.End {
|
||||
end = hub.End
|
||||
}
|
||||
// Project policy follows static EndDateType: play content closes at
|
||||
// PlayEnd; exchange content can remain through the declared End.
|
||||
if num(slot, 4) == 0 && end > hub.PlayEnd {
|
||||
end = hub.PlayEnd
|
||||
}
|
||||
if end <= start {
|
||||
continue
|
||||
}
|
||||
b := wire.AppendVarint(nil, 1, num(slot, 10))
|
||||
b = wire.AppendVarint(b, 2, contentType)
|
||||
b = wire.AppendVarint(b, 3, contentID)
|
||||
b = wire.AppendVarint(b, 4, child.UID)
|
||||
b = wire.AppendVarint(b, 5, uint64(start))
|
||||
b = wire.AppendVarint(b, 6, uint64(end))
|
||||
v = wire.AppendBytes(v, 6, b)
|
||||
}
|
||||
out = wire.AppendBytes(out, 1, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Hub content and EventType are separate client enums. MiniGame schedules
|
||||
// are design identities; calendar SubID is zero for the supported slot routes.
|
||||
func (s *Service) miniSlotSchedule(contentType, id uint64) (uint64, uint64, bool) {
|
||||
eventType, known := gamedata.MiniHubEventType(contentType)
|
||||
if !known {
|
||||
return 0, 0, false
|
||||
}
|
||||
if contentType == 6 {
|
||||
if _, err := s.design.Row("PackEventMiniGameTable", 8, id); err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
}
|
||||
return eventType, 0, true
|
||||
}
|
||||
|
||||
// Public event packs retain their declared wire settings and both end windows.
|
||||
// Mini hub prefabs are served exclusively by MiniEventHubInfo.
|
||||
func (s *Service) publicEventHubs(req []byte) ([]byte, error) {
|
||||
_, body, _, err := s.hubCalendars.Handle("/EventHubInfo", req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []byte
|
||||
err = wire.Walk(body, func(f wire.Field) error {
|
||||
if f.Number != 1 || f.Type != 2 {
|
||||
return nil
|
||||
}
|
||||
row, err := s.design.Row("PackEventHubTable", 14, num(f.Value, 2))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if num(row, 13) == 0 {
|
||||
out = wire.AppendBytes(out, 1, f.Value)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package eventplay
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/events"
|
||||
"bd2server/internal/server/readonly"
|
||||
"bd2server/internal/server/wire"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func hubField(n int, v uint64) readonly.Field { return readonly.Field{Number: n, Type: 0, Varint: v} }
|
||||
func miniCalendar(uid, hub, start, playEnd, end uint64, refs ...uint64) readonly.Field {
|
||||
settings := []readonly.Field{hubField(1, 5), hubField(2, 5)}
|
||||
for _, uid := range refs {
|
||||
settings = append(settings, hubField(3, uid))
|
||||
}
|
||||
return readonly.Field{Number: 1, Type: 2, Fields: []readonly.Field{hubField(1, uid), hubField(2, hub), hubField(3, start), hubField(4, playEnd), hubField(5, end), {Number: 6, Type: 2, Fields: settings}}}
|
||||
}
|
||||
func attachMiniCalendar(s *Service, rows ...readonly.Field) {
|
||||
s.AttachHubCalendars(&readonly.Seed{Responses: map[string]readonly.Response{"/EventHubInfo": {Fields: rows}}})
|
||||
}
|
||||
func miniSlot(group, id, index, typ, content, endType uint64) []byte {
|
||||
var out []byte
|
||||
for _, x := range [][2]uint64{{6, group}, {10, id}, {11, index}, {9, typ}, {7, content}, {4, endType}} {
|
||||
out = wire.AppendVarint(out, int(x[0]), x[1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
func TestMiniHubStaticSlotAndExplicitScheduleIdentity(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
hub := wire.AppendVarint(nil, 14, 1003)
|
||||
hub = wire.AppendVarint(hub, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{hub}
|
||||
s.design.Tables["PackEventListTable"] = [][]byte{miniSlot(1003, 4, 5, 5, 17, 1), miniSlot(1003, 2, 11, 13, 3, 0)}
|
||||
registry := events.NewRegistry()
|
||||
if err := registry.Replace([]events.Schedule{
|
||||
{UID: 40, Type: 12, ID: 17, Start: 100, End: 300},
|
||||
{UID: 41, Type: 12, ID: 17, Start: 400, End: 600},
|
||||
{UID: 42, Type: 13, ID: 17, Start: 100, End: 300},
|
||||
{UID: 43, Type: 12, ID: 17, SubID: 1003, Start: 100, End: 300},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.registry = registry
|
||||
attachMiniCalendar(s, miniCalendar(1, 1003, 100, 200, 300, 40, 41), miniCalendar(2, 1003, 400, 500, 600, 40, 41))
|
||||
for _, now := range []int64{99, 100, 200, 300, 601} {
|
||||
s.now = func() time.Time { return time.UnixMilli(now) }
|
||||
_, body, _, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := 0
|
||||
_ = wire.Walk(body, func(f wire.Field) error {
|
||||
if f.Number != 1 {
|
||||
return nil
|
||||
}
|
||||
count++
|
||||
slot, ok, err := wire.Bytes(f.Value, 6)
|
||||
if err != nil || !ok || num(slot, 1) != 4 || num(slot, 2) != 5 || num(slot, 3) != 17 {
|
||||
t.Fatalf("bad static mapping %x", slot)
|
||||
}
|
||||
want := uint64(40)
|
||||
if num(f.Value, 1) == 2 {
|
||||
want = 41
|
||||
}
|
||||
if num(slot, 4) != want || num(slot, 6) != num(f.Value, 5) {
|
||||
t.Fatalf("bad UID/window %x", slot)
|
||||
}
|
||||
if num(f.Value, 4) == num(f.Value, 5) {
|
||||
t.Fatal("play/end window lost")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if count != 2 || len(s.state.Replies) != 0 {
|
||||
t.Fatalf("calendar count=%d cached=%d", count, len(s.state.Replies))
|
||||
}
|
||||
}
|
||||
// Same sequence must reflect an updated published calendar immediately.
|
||||
attachMiniCalendar(s, miniCalendar(2, 1003, 400, 500, 600, 41))
|
||||
_, body, _, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _, _ := wire.Bytes(body, 1)
|
||||
if num(raw, 1) != 2 {
|
||||
t.Fatal("stale account reply returned")
|
||||
}
|
||||
}
|
||||
func TestMiniHubRequiresExplicitCalendarAndRejectsAmbiguousSlot(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
hub := wire.AppendVarint(nil, 14, 1003)
|
||||
hub = wire.AppendVarint(hub, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{hub}
|
||||
s.design.Tables["PackEventListTable"] = [][]byte{miniSlot(1003, 4, 5, 5, 17, 1)}
|
||||
registry := events.NewRegistry()
|
||||
_ = registry.Replace([]events.Schedule{{UID: 1, Type: 8, ID: 2010, Start: 100, End: 300}, {UID: 40, Type: 12, ID: 17, Start: 100, End: 300}, {UID: 41, Type: 12, ID: 17, Start: 100, End: 300}})
|
||||
s.registry = registry
|
||||
_, body, _, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil || len(body) != 0 {
|
||||
t.Fatal("hub guessed from pack schedule", err)
|
||||
}
|
||||
attachMiniCalendar(s, miniCalendar(1, 1003, 100, 200, 300, 40, 41))
|
||||
if _, _, _, err = s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session"); err == nil {
|
||||
t.Fatal("ambiguous slot silently selected")
|
||||
}
|
||||
if len(s.state.Replies) != 0 {
|
||||
t.Fatal("failed public query persisted")
|
||||
}
|
||||
}
|
||||
func TestMiniHubExplicitInvalidBindingIsRejected(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
typ, sub, progress uint64
|
||||
}{{"wrong event type", 13, 0, 5}, {"wrong sub id", 12, 1003, 5}, {"wrong content type", 12, 0, 3}} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
hub := wire.AppendVarint(nil, 14, 1003)
|
||||
hub = wire.AppendVarint(hub, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{hub}
|
||||
s.design.Tables["PackEventListTable"] = [][]byte{miniSlot(1003, 4, 5, 5, 17, 1)}
|
||||
registry := events.NewRegistry()
|
||||
_ = registry.Replace([]events.Schedule{{UID: 40, Type: tc.typ, ID: 17, SubID: tc.sub, Start: 100, End: 300}})
|
||||
s.registry = registry
|
||||
field := miniCalendar(1, 1003, 100, 200, 300, 40)
|
||||
field.Fields[5].Fields[1] = hubField(2, tc.progress)
|
||||
attachMiniCalendar(s, field)
|
||||
if _, _, _, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session"); err == nil {
|
||||
t.Fatal("invalid explicit binding accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestNormalAndMiniHubCalendarsUseSeparatePrefabs(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
normal := wire.AppendVarint(nil, 14, 58)
|
||||
mini := wire.AppendVarint(nil, 14, 1003)
|
||||
mini = wire.AppendVarint(mini, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{normal, mini}
|
||||
// No settings here: the calendars still expose their complete windows.
|
||||
a := miniCalendar(1, 58, 100, 200, 300)
|
||||
a.Fields = a.Fields[:5]
|
||||
b := miniCalendar(2, 1003, 400, 500, 600)
|
||||
b.Fields = b.Fields[:5]
|
||||
attachMiniCalendar(s, a, b)
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
uid uint64
|
||||
}{{"/EventHubInfo", 1}, {"/MiniEventHubInfo", 2}} {
|
||||
_, body, _, err := s.HandleSession(tc.path, wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := 0
|
||||
_ = wire.Walk(body, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
count++
|
||||
if num(f.Value, 1) != tc.uid || num(f.Value, 4) == num(f.Value, 5) {
|
||||
t.Fatalf("bad calendar %s %x", tc.path, f.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if count != 1 {
|
||||
t.Fatal("hub appeared in both prefab routes", tc.path, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestMiniHubExplicitUnsupportedQuizIsRejected(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
hub := wire.AppendVarint(nil, 14, 1003)
|
||||
hub = wire.AppendVarint(hub, 13, 1)
|
||||
s.design.Tables["PackEventHubTable"] = [][]byte{hub}
|
||||
s.design.Tables["PackEventListTable"] = [][]byte{miniSlot(1003, 2, 11, 13, 3, 0)}
|
||||
field := miniCalendar(1, 1003, 100, 200, 300, 40)
|
||||
field.Fields[5].Fields[0] = hubField(1, 11)
|
||||
field.Fields[5].Fields[1] = hubField(2, 13)
|
||||
attachMiniCalendar(s, field)
|
||||
if _, _, _, err := s.HandleSession("/MiniEventHubInfo", wire.AppendVarint(nil, 1, 1), "session"); err == nil {
|
||||
t.Fatal("unsupported quiz enabled with invented UID")
|
||||
}
|
||||
}
|
||||
func TestPublicHubsWithoutProjectCalendarDoNotGuessSchedules(t *testing.T) {
|
||||
s, _ := makeService(t)
|
||||
registry := events.NewRegistry()
|
||||
if err := registry.Replace([]events.Schedule{{UID: 1, Type: 8, ID: 2010, Start: 100, End: 300}, {UID: 2, Type: 11, ID: 41, Start: 100, End: 300}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.registry = registry
|
||||
for _, path := range []string{"/EventHubInfo", "/MiniEventHubInfo", "/MiniGameHubInfo"} {
|
||||
code, body, handled, err := s.HandleSession(path, wire.AppendVarint(nil, 1, 1), "session")
|
||||
if err != nil || !handled || code != codes[path] || len(body) != 0 {
|
||||
t.Fatalf("path=%s code=%d handled=%t body=%x err=%v", path, code, handled, body, err)
|
||||
}
|
||||
}
|
||||
if len(s.state.Replies) != 0 {
|
||||
t.Fatal("public calendar absence persisted")
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,21 @@ func (s *Service) HandleSession(path string, req []byte, session string) (int, [
|
||||
}); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
// Public hub calendars are derived afresh, never stored as account replies.
|
||||
if path == "/EventHubInfo" || path == "/MiniEventHubInfo" || path == "/MiniGameHubInfo" {
|
||||
if s.hubCalendars != nil && path == "/MiniGameHubInfo" {
|
||||
return s.hubCalendars.Handle(path, req)
|
||||
}
|
||||
if s.hubCalendars != nil && path == "/EventHubInfo" {
|
||||
out, err := s.publicEventHubs(req)
|
||||
return code, out, true, err
|
||||
}
|
||||
if path == "/MiniEventHubInfo" {
|
||||
out, err := s.miniEventHubs(req)
|
||||
return code, out, true, err
|
||||
}
|
||||
return code, nil, true, nil
|
||||
}
|
||||
key := fmt.Sprintf("%s:%s:%d", session, path, seq)
|
||||
if r, ok := s.state.Replies[key]; ok {
|
||||
if !bytes.Equal(r.Request, req) {
|
||||
@@ -171,11 +186,7 @@ func (s *Service) HandleSession(path string, req []byte, session string) (int, [
|
||||
out = summaryWire(&next, path == "/MiniGameUserRecordInfo")
|
||||
case "/MiniGameRelayServerInfo":
|
||||
return fail(fmt.Errorf("eventplay: native relay channels are not configured"))
|
||||
case "/EventHubInfo", "/MiniEventHubInfo", "/MiniGameHubInfo":
|
||||
if s.hubCalendars != nil && path != "/MiniEventHubInfo" {
|
||||
return s.hubCalendars.Handle(path, req)
|
||||
}
|
||||
out, e = s.hubs(path)
|
||||
|
||||
case "/PackEventStoryInfo", "/PackEventBattleInfo":
|
||||
uids, err := list(req, 2)
|
||||
if err != nil {
|
||||
@@ -274,72 +285,6 @@ func (s *Service) grant(identity string, rewards []gamedata.BattleReward) ([]byt
|
||||
}
|
||||
return s.economy.Apply(identity, nil, rs)
|
||||
}
|
||||
func (s *Service) hubs(path string) ([]byte, error) {
|
||||
var out []byte
|
||||
rows := s.registry.List()
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].UID < rows[j].UID })
|
||||
for _, c := range rows {
|
||||
if c.Type != 8 && c.Type != 11 {
|
||||
continue
|
||||
}
|
||||
if path == "/MiniGameHubInfo" {
|
||||
if c.Type != 11 {
|
||||
continue
|
||||
}
|
||||
v := wire.AppendVarint(nil, 1, c.SubID)
|
||||
v = wire.AppendVarint(v, 2, c.UID)
|
||||
v = wire.AppendVarint(v, 3, 1)
|
||||
out = wire.AppendBytes(out, 1, v)
|
||||
continue
|
||||
}
|
||||
if path == "/MiniEventHubInfo" {
|
||||
v := wire.AppendVarint(nil, 1, c.UID)
|
||||
v = wire.AppendVarint(v, 2, c.ID)
|
||||
v = wire.AppendVarint(v, 3, uint64(c.Start))
|
||||
v = wire.AppendVarint(v, 4, uint64(c.End))
|
||||
v = wire.AppendVarint(v, 5, uint64(c.End))
|
||||
for _, child := range rows {
|
||||
if child.UID == c.UID || child.SubID != c.ID {
|
||||
continue
|
||||
}
|
||||
slot := wire.AppendVarint(nil, 1, child.SubID)
|
||||
slot = wire.AppendVarint(slot, 2, child.Type)
|
||||
slot = wire.AppendVarint(slot, 3, child.ID)
|
||||
slot = wire.AppendVarint(slot, 4, child.UID)
|
||||
slot = wire.AppendVarint(slot, 5, uint64(child.Start))
|
||||
slot = wire.AppendVarint(slot, 6, uint64(child.End))
|
||||
v = wire.AppendBytes(v, 6, slot)
|
||||
}
|
||||
out = wire.AppendBytes(out, 1, v)
|
||||
continue
|
||||
}
|
||||
hub, e := s.design.Row("PackEventHubTable", 14, c.ID)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
_ = hub
|
||||
v := wire.AppendVarint(nil, 1, c.UID)
|
||||
v = wire.AppendVarint(v, 2, c.ID)
|
||||
v = wire.AppendVarint(v, 3, uint64(c.Start))
|
||||
v = wire.AppendVarint(v, 4, uint64(c.End))
|
||||
v = wire.AppendVarint(v, 5, uint64(c.End))
|
||||
for _, r := range s.design.Rows("PackEventListTable", 6, c.ID) {
|
||||
typ, id, slot := num(r, 9), num(r, 7), num(r, 11)
|
||||
for _, child := range rows {
|
||||
if child.ID != id {
|
||||
continue
|
||||
}
|
||||
setting := wire.AppendVarint(nil, 1, slot)
|
||||
setting = wire.AppendVarint(setting, 2, typ)
|
||||
setting = wire.AppendVarint(setting, 3, child.UID)
|
||||
v = wire.AppendBytes(v, 6, setting)
|
||||
break
|
||||
}
|
||||
}
|
||||
out = wire.AppendBytes(out, 1, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func recordWire(r record) []byte {
|
||||
b := wire.AppendVarint(nil, 1, r.UID)
|
||||
b = wire.AppendVarint(b, 2, r.Paid)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package gamedata
|
||||
|
||||
// MiniHubEventType maps PackEventListTable.HubContentType to the independent
|
||||
// EventSchedule enum used by playable slot handlers. Quiz and daily story have
|
||||
// no global event type and cannot be bound by borrowing another game's UID.
|
||||
func MiniHubEventType(contentType uint64) (uint64, bool) {
|
||||
switch contentType {
|
||||
case 4:
|
||||
return 7, true
|
||||
case 5:
|
||||
return 12, true
|
||||
case 6:
|
||||
return 11, true
|
||||
case 7:
|
||||
return 13, true
|
||||
case 11:
|
||||
return 17, true
|
||||
case 12:
|
||||
return 19, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using BD2.GameNames;
|
||||
using Google.Protobuf;
|
||||
using HarmonyLib;
|
||||
using Proto.Net;
|
||||
using UnityEngine;
|
||||
using gamfs;
|
||||
using static Bd2LoginUI.LoginRuntime;
|
||||
using static Bd2LoginUI.SessionRecovery;
|
||||
|
||||
namespace Bd2LoginUI;
|
||||
|
||||
// An authoritative empty hub response must also remove presentation cached by
|
||||
// the client. The original receiver returns before doing its normal reset.
|
||||
internal static class EventHubPresentation
|
||||
{
|
||||
internal static void Install(Harmony harmony)
|
||||
{
|
||||
MethodInfo receive = typeof(MiniEventHubPacket).GetGameMethod(
|
||||
"RecvMiniEventHubInfoResponse", BindingFlags.Static | BindingFlags.NonPublic,
|
||||
null, new[] { typeof(IMessage), typeof(int), typeof(int), typeof(Action) }, null);
|
||||
if (receive == null || receive.ReturnType != typeof(bool))
|
||||
throw new MissingMethodException("MiniEventHubPacket.RecvMiniEventHubInfoResponse(IMessage) was not found");
|
||||
harmony.Patch(receive, postfix: new HarmonyMethod(typeof(EventHubPresentation), nameof(ReceivePostfix)));
|
||||
Log?.LogInfo("Authoritative empty mini-event hub presentation reset installed");
|
||||
}
|
||||
|
||||
private static void ReceivePostfix(IMessage __0, int __2, bool __result)
|
||||
{
|
||||
if (ServerRoot == null || !EstablishedGameSession || !__result || __2 > 0 ||
|
||||
!(__0 is MiniEventHubInfoResponse response) || response.MiniEventHubInfo.Count != 0) return;
|
||||
try
|
||||
{
|
||||
// Detach the old manager subscriber before Reset removes its UID.
|
||||
// The client's scheduler exposes subscriber removal, but no global
|
||||
// mini-hub-only disposal API; do not disturb unrelated game timers.
|
||||
foreach (MiniEventHubDBInfo hub in MiniEventHubPacket.MiniEventHubDBInfoes)
|
||||
if (hub != null && SG.Schedule != null)
|
||||
SG.Schedule.RemoveMiniEventHubChangeState(hub.Uid, "MiniEventHubManager");
|
||||
MiniEventHubPacket.Reset();
|
||||
MiniEventHubManager.SetCurrentEventHub();
|
||||
// Close only the stale hub window through the normal UI lifecycle.
|
||||
// CloseProcess releases its slots and resumes the menu presentation.
|
||||
MiniEventMainUI hubUI = UIManager.GetUI<MiniEventMainUI>();
|
||||
if (hubUI != null) hubUI.CloseUI();
|
||||
// UpdateUI clears its own old subscriptions and closes the button
|
||||
// when CurrentEventHubDBInfo is null. SetMiniEventHub would send a
|
||||
// new request, so it must not be called while receiving this one.
|
||||
foreach (MenuUI_MiniEventHubButton button in UnityEngine.Object.FindObjectsOfType<MenuUI_MiniEventHubButton>(true))
|
||||
if (button != null)
|
||||
{
|
||||
try { button.UpdateUI(); }
|
||||
catch (Exception error) { Log?.LogWarning("Empty mini-event hub menu refresh failed: " + error.GetType().Name); }
|
||||
}
|
||||
Log?.LogInfo("Empty mini-event hub response cleared cached hub selection and menu presentation");
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Log?.LogWarning("Empty mini-event hub presentation reset failed: " + error.GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
Harmony harmony = new Harmony(Guid);
|
||||
SessionDiagnostics.Install(harmony);
|
||||
EventRequestDiagnostics.Install(harmony);
|
||||
EventHubPresentation.Install(harmony);
|
||||
harmony.Patch(awake, postfix: new HarmonyMethod(typeof(SessionRecovery), nameof(IntroAwakePostfix)));
|
||||
HarmonyMethod maintenancePrefix = new HarmonyMethod(typeof(LoginController), nameof(SendMaintenancePrefix));
|
||||
maintenancePrefix.after = new[] { "bd2.localidentity" };
|
||||
|
||||
Reference in New Issue
Block a user