fix(all): route activity hubs by explicit calendars and clear stale mini event UI

This commit is contained in:
2026-10-05 14:47:28 +08:00
parent 58716cfe25
commit f65e45ee42
12 changed files with 793 additions and 72 deletions
+3
View File
@@ -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 {
+98
View File
@@ -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")
}
}
+2
View File
@@ -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")
}
}
+16 -71
View File
@@ -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)
+23
View File
@@ -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
}
}
+63
View File
@@ -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);
}
}
}
+1
View File
@@ -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" };