feat(server): add special loot and data-driven gameplay rules

Generate equipment loot atomically, grant random boxes and prevent repeat claims.
Read gacha pools, pity, products, tickets, missions, presets, chapter rewards
and inventory costs from GameData; extend both collaboration UR ticket pools.
Sync owned recipes, configure startup state and calendars in versioned seeds,
and validate unlock tickets and revival talents.
This commit is contained in:
2026-10-04 19:15:59 +08:00
parent 64454e71a3
commit 5b5a219dc2
86 changed files with 3577 additions and 646 deletions
+44 -2
View File
@@ -1,13 +1,55 @@
package schedule
import (
"os"
"path/filepath"
"testing"
"bd2server/internal/server/wire"
)
func TestCalendarReloadsChangedIDsAndTimesFromFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "schedule.json")
for _, revision := range []struct {
id, start, end uint64
raw string
}{
{709, 101, 303, `{"version":"2.35.10","calculate_milliseconds":17,"contents":[{"id":81,"current":{"id":709,"start_milliseconds":101,"end_milliseconds":303},"next":{"id":710,"start_milliseconds":401,"end_milliseconds":501}}],"regular":[{"content_id":81,"season":9}]}`},
{811, 701, 903, `{"version":"2.35.10","calculate_milliseconds":17,"contents":[{"id":81,"current":{"id":811,"start_milliseconds":701,"end_milliseconds":903},"next":{"id":812,"start_milliseconds":1001,"end_milliseconds":1201}}],"regular":[{"content_id":81,"season":11}]}`},
} {
if err := os.WriteFile(path, []byte(revision.raw), 0600); err != nil {
t.Fatal(err)
}
service, err := Load(path)
if err != nil {
t.Fatal(err)
}
_, response, _, err := service.Handle("/ScheduleInfo", wire.AppendVarint(nil, 1, 1))
if err != nil {
t.Fatal(err)
}
content, _, _ := wire.Bytes(response, 2)
current, _, _ := wire.Bytes(content, 2)
id, _, _ := wire.Varint(current, 1)
start, _, _ := wire.Varint(current, 2)
end, _, _ := wire.Varint(current, 3)
if id != revision.id || start != revision.start || end != revision.end {
t.Fatalf("season=%d times=%d/%d", id, start, end)
}
}
if err := os.WriteFile(path, []byte(`{"version":"2.35.10","calculate_milliseconds":1,"contents":[{"id":1,"current":{"id":2,"start_milliseconds":4,"end_milliseconds":3}}]}`), 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil {
t.Fatal("reversed date accepted")
}
}
func TestVersion23510ContainsRequiredContentSix(t *testing.T) {
service := Current()
service, err := Load("../../../seed/v2_35_10/schedule.json")
if err != nil {
t.Fatal(err)
}
code, response, handled, err := service.Handle("/ScheduleInfo", wire.AppendVarint(nil, 1, 1))
if err != nil || !handled || code != 117 {
t.Fatalf("schedule code=%d handled=%v err=%v", code, handled, err)
@@ -32,7 +74,7 @@ func TestVersion23510ContainsRequiredContentSix(t *testing.T) {
}
func TestScheduleRejectsMissingSequence(t *testing.T) {
if _, _, handled, err := Current().Handle("/ScheduleInfo", nil); !handled || err == nil {
if _, _, handled, err := (&Service{}).Handle("/ScheduleInfo", nil); !handled || err == nil {
t.Fatalf("missing sequence handled=%v err=%v", handled, err)
}
}