mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Implement world-map entities
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -3,57 +3,463 @@ package masterdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MaxUserGimmickRows is the server's per-table cap for gimmick projections and the
|
||||||
|
// in-memory user.Gimmick.Sequences map. The client hard-caps each of its
|
||||||
|
// IUserGimmick* tables at 1024 rows; we project up to this many with a small
|
||||||
|
// safety margin so we never overflow client buffers. Shared by InitSequenceSchedule
|
||||||
|
// (limits user.Gimmick.Sequences map size) and the IUserGimmick / Ornament /
|
||||||
|
// Sequence projections.
|
||||||
|
const MaxUserGimmickRows = 1000
|
||||||
|
|
||||||
type gimmickScheduleEntry struct {
|
type gimmickScheduleEntry struct {
|
||||||
ScheduleId int32
|
ScheduleId int32
|
||||||
StartDatetime int64
|
StartDatetime int64
|
||||||
EndDatetime int64
|
EndDatetime int64
|
||||||
FirstSequenceId int32
|
FirstSequenceId int32
|
||||||
RequiredQuestId int32 // 0 = always active
|
RequiredQuestId int32 // 0 = always active
|
||||||
|
IsHidden bool // hidden-story or cage-memory gimmick: bypasses the quest gate
|
||||||
|
Rank int // trim priority — see gimmickTypeRank
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGimmickTable[T any](name, what string) ([]T, bool) {
|
||||||
|
rows, err := utils.ReadTable[T](name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[gimmick] %s unavailable, %s empty: %v", name, what, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return rows, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func gimmickTypeRank(t model.GimmickType) int {
|
||||||
|
switch t {
|
||||||
|
case model.GimmickTypeReport: // hidden missions / stories
|
||||||
|
return 0
|
||||||
|
case model.GimmickTypeCageMemory: // lost archives
|
||||||
|
return 1
|
||||||
|
case model.GimmickTypeCageTreasureHunt: // treasure
|
||||||
|
return 2
|
||||||
|
case model.GimmickTypeBrokenObelisk, model.GimmickTypeFirstBrokenObelisk:
|
||||||
|
return 3
|
||||||
|
case model.GimmickTypeIronGrill:
|
||||||
|
return 4
|
||||||
|
case model.GimmickTypeRadioMessage:
|
||||||
|
return 5
|
||||||
|
case model.GimmickTypeMapOnlyCageTreasureHunt, model.GimmickTypeMapOnlyHideObelisk:
|
||||||
|
return 6
|
||||||
|
case model.GimmickTypeCageIntervalDropItem, model.GimmickTypeMapOnlyCageIntervalDrop:
|
||||||
|
return 7 // birds — bottom
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
|
||||||
|
type gimmickTypeTables struct {
|
||||||
|
byGimmick map[int32]model.GimmickType
|
||||||
|
bySequence map[int32]model.GimmickType
|
||||||
|
}
|
||||||
|
|
||||||
|
var gimmickTypes = sync.OnceValue(loadGimmickTypes)
|
||||||
|
|
||||||
|
func loadGimmickTypes() gimmickTypeTables {
|
||||||
|
empty := gimmickTypeTables{
|
||||||
|
byGimmick: map[int32]model.GimmickType{},
|
||||||
|
bySequence: map[int32]model.GimmickType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickGroup]("m_gimmick_group", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
byGimmick := make(map[int32]model.GimmickType, len(gimmicks))
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
byGimmick[g.GimmickId] = model.GimmickType(g.GimmickType)
|
||||||
|
}
|
||||||
|
typeByGroup := make(map[int32]model.GimmickType, len(groups))
|
||||||
|
for _, grp := range groups {
|
||||||
|
if _, seen := typeByGroup[grp.GimmickGroupId]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, ok := byGimmick[grp.GimmickId]; ok {
|
||||||
|
typeByGroup[grp.GimmickGroupId] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bySequence := make(map[int32]model.GimmickType, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
if t, ok := typeByGroup[seq.GimmickGroupId]; ok {
|
||||||
|
bySequence[seq.GimmickSequenceId] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gimmickTypeTables{byGimmick: byGimmick, bySequence: bySequence}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gimmickSequenceTypes() map[int32]model.GimmickType {
|
||||||
|
return gimmickTypes().bySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickSequenceRanks() map[int32]int {
|
||||||
|
types := gimmickSequenceTypes()
|
||||||
|
out := make(map[int32]int, len(types))
|
||||||
|
for sid, t := range types {
|
||||||
|
out[sid] = gimmickTypeRank(t)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type SequenceReward struct {
|
||||||
|
PossessionType int32
|
||||||
|
PossessionId int32
|
||||||
|
Count int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type GimmickCatalog struct {
|
type GimmickCatalog struct {
|
||||||
schedules []gimmickScheduleEntry
|
schedules []gimmickScheduleEntry
|
||||||
|
hiddenSequences map[int32]bool // GimmickSequenceId -> report/cage-memory
|
||||||
|
sequenceRewards map[int32][]SequenceReward // GimmickSequenceId -> clear rewards
|
||||||
|
gimmickTypes map[int32]model.GimmickType
|
||||||
|
cageMemoryItems map[int32]int32 // CageMemory GimmickId -> ImportantItemId (type 4)
|
||||||
|
hiddenBirdRewards map[GimmickOrnamentRef]SequenceReward
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadGimmickCatalog(resolver *ConditionResolver) (*GimmickCatalog, error) {
|
func LoadGimmickCatalog(resolver *ConditionResolver, cageOrnaments *CageOrnamentCatalog) (*GimmickCatalog, error) {
|
||||||
rows, err := utils.ReadTable[EntityMGimmickSequenceSchedule]("m_gimmick_sequence_schedule")
|
rows, err := utils.ReadTable[EntityMGimmickSequenceSchedule]("m_gimmick_sequence_schedule")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
|
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]gimmickScheduleEntry, 0, len(rows))
|
seqTypes := gimmickSequenceTypes()
|
||||||
|
hiddenSeq := make(map[int32]bool, len(seqTypes))
|
||||||
|
for sid, t := range seqTypes {
|
||||||
|
if t == model.GimmickTypeReport || t == model.GimmickTypeCageMemory {
|
||||||
|
hiddenSeq[sid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick rule: prefer schedules with EndDatetime in the future (so the client's
|
||||||
|
// in-cage filter doesn't drop them), then by lowest StartDatetime (longest
|
||||||
|
// historical validity, tends to carry the simplest RequiredQuestId), tie-broken
|
||||||
|
// by lowest ScheduleId for determinism. The future-end preference matters for
|
||||||
|
// "Fickle Black Birds" (type 1) where real expiry dates vary; other types have
|
||||||
|
// EndDatetime = 9999-03-31 so the preference is a no-op.
|
||||||
|
now := gametime.NowMillis()
|
||||||
|
bestBySeq := make(map[int32]gimmickScheduleEntry, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
entry := gimmickScheduleEntry{
|
entry := gimmickScheduleEntry{
|
||||||
ScheduleId: r.GimmickSequenceScheduleId,
|
ScheduleId: r.GimmickSequenceScheduleId,
|
||||||
StartDatetime: r.StartDatetime,
|
StartDatetime: r.StartDatetime,
|
||||||
EndDatetime: r.EndDatetime,
|
EndDatetime: r.EndDatetime,
|
||||||
FirstSequenceId: r.FirstGimmickSequenceId,
|
FirstSequenceId: r.FirstGimmickSequenceId,
|
||||||
|
IsHidden: hiddenSeq[r.FirstGimmickSequenceId],
|
||||||
|
Rank: gimmickTypeRank(seqTypes[r.FirstGimmickSequenceId]),
|
||||||
}
|
}
|
||||||
if r.ReleaseEvaluateConditionId != 0 {
|
if r.ReleaseEvaluateConditionId != 0 {
|
||||||
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
||||||
entry.RequiredQuestId = qid
|
entry.RequiredQuestId = qid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries = append(entries, entry)
|
if existing, ok := bestBySeq[entry.FirstSequenceId]; ok {
|
||||||
|
existingFuture := existing.EndDatetime > now
|
||||||
|
entryFuture := entry.EndDatetime > now
|
||||||
|
if existingFuture != entryFuture {
|
||||||
|
// Future-end schedule wins over expired one.
|
||||||
|
if existingFuture {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if existing.StartDatetime < entry.StartDatetime ||
|
||||||
|
(existing.StartDatetime == entry.StartDatetime && existing.ScheduleId <= entry.ScheduleId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bestBySeq[entry.FirstSequenceId] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("gimmick catalog loaded: %d schedules", len(entries))
|
entries := make([]gimmickScheduleEntry, 0, len(bestBySeq))
|
||||||
return &GimmickCatalog{schedules: entries}, nil
|
hiddenCount := 0
|
||||||
|
for _, entry := range bestBySeq {
|
||||||
|
if entry.IsHidden {
|
||||||
|
hiddenCount++
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
dedupedCount := len(rows) - len(entries)
|
||||||
|
|
||||||
|
// Sort by (Rank, ScheduleId) so ActiveScheduleKeys returns the priority order
|
||||||
|
// directly: treasure/lost-archives/hidden-missions first, birds last. This is
|
||||||
|
// what lets InitSequenceSchedule's 1000-row cap trim from the bottom.
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
if entries[i].Rank != entries[j].Rank {
|
||||||
|
return entries[i].Rank < entries[j].Rank
|
||||||
|
}
|
||||||
|
return entries[i].ScheduleId < entries[j].ScheduleId
|
||||||
|
})
|
||||||
|
|
||||||
|
sequenceRewards := loadGimmickSequenceRewards()
|
||||||
|
cageMemoryItems := loadCageMemoryImportantItems(gimmickTypes().byGimmick)
|
||||||
|
hiddenBirdRewards := loadHiddenBirdRewards(cageOrnaments)
|
||||||
|
|
||||||
|
log.Printf("gimmick catalog loaded: %d schedules (%d hidden-content, %d duplicates dropped), %d reward sequences, %d cage-memory items, %d hidden-bird rewards",
|
||||||
|
len(entries), hiddenCount, dedupedCount, len(sequenceRewards), len(cageMemoryItems), len(hiddenBirdRewards))
|
||||||
|
return &GimmickCatalog{
|
||||||
|
schedules: entries,
|
||||||
|
hiddenSequences: hiddenSeq,
|
||||||
|
sequenceRewards: sequenceRewards,
|
||||||
|
gimmickTypes: gimmickTypes().byGimmick,
|
||||||
|
cageMemoryItems: cageMemoryItems,
|
||||||
|
hiddenBirdRewards: hiddenBirdRewards,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiddenBirdReward returns the per-tap reward for a MAP_ONLY_CAGE_TREASURE_HUNT
|
||||||
|
// ("Hidden Black Birds", type 7) ornament. Returns false if there's no mapping
|
||||||
|
// (e.g. the ornament view has no corresponding cage-ornament-reward entry).
|
||||||
|
func (c *GimmickCatalog) HiddenBirdReward(gimmickId, ornamentIndex int32) (SequenceReward, bool) {
|
||||||
|
r, ok := c.hiddenBirdRewards[GimmickOrnamentRef{GimmickId: gimmickId, OrnamentIndex: ornamentIndex}]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadHiddenBirdRewards resolves (GimmickId, OrnamentIndex) -> CageOrnamentReward for
|
||||||
|
// every type-7 ("Hidden Black Birds") gimmick. The mapping is structural:
|
||||||
|
//
|
||||||
|
// m_gimmick (GimmickType == 7) -> GimmickOrnamentGroupId
|
||||||
|
// m_gimmick_ornament (matching group) -> GimmickOrnamentViewId
|
||||||
|
// m_cage_ornament (CageOrnamentId == ViewId) -> CageOrnamentRewardId
|
||||||
|
// m_cage_ornament_reward (matching id) -> PossessionType / PossessionId / Count
|
||||||
|
//
|
||||||
|
// 110 of 114 type-7 ornaments have a matching m_cage_ornament row in the current
|
||||||
|
// data; the rest log a warning and are silently skipped so the player just gets
|
||||||
|
// no reward on those (no crash).
|
||||||
|
func loadHiddenBirdRewards(cageOrnaments *CageOrnamentCatalog) map[GimmickOrnamentRef]SequenceReward {
|
||||||
|
empty := map[GimmickOrnamentRef]SequenceReward{}
|
||||||
|
if cageOrnaments == nil {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "hidden-bird rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "hidden-bird rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicksByGroup := make(map[int32][]int32)
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
if model.GimmickType(g.GimmickType) == model.GimmickTypeMapOnlyCageTreasureHunt {
|
||||||
|
gimmicksByGroup[g.GimmickOrnamentGroupId] = append(gimmicksByGroup[g.GimmickOrnamentGroupId], g.GimmickId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[GimmickOrnamentRef]SequenceReward)
|
||||||
|
missing := 0
|
||||||
|
for _, o := range ornaments {
|
||||||
|
gids, ok := gimmicksByGroup[o.GimmickOrnamentGroupId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reward, ok := cageOrnaments.LookupReward(o.GimmickOrnamentViewId)
|
||||||
|
if !ok {
|
||||||
|
missing++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := SequenceReward{
|
||||||
|
PossessionType: reward.PossessionType,
|
||||||
|
PossessionId: reward.PossessionId,
|
||||||
|
Count: reward.Count,
|
||||||
|
}
|
||||||
|
for _, gid := range gids {
|
||||||
|
out[GimmickOrnamentRef{GimmickId: gid, OrnamentIndex: o.GimmickOrnamentIndex}] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing > 0 {
|
||||||
|
log.Printf("[gimmick] %d hidden-bird ornaments had no m_cage_ornament_reward row", missing)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) GimmickType(gimmickId int32) model.GimmickType {
|
||||||
|
return c.gimmickTypes[gimmickId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CageMemoryImportantItem returns the ImportantItemId (type 4) that the library uses
|
||||||
|
// to mark a tapped cage memory as collected, given the world-gimmick id. The mapping
|
||||||
|
// is derived from m_gimmick_additional_asset texture suffixes — see
|
||||||
|
// loadCageMemoryImportantItems.
|
||||||
|
func (c *GimmickCatalog) CageMemoryImportantItem(gimmickId int32) (int32, bool) {
|
||||||
|
id, ok := c.cageMemoryItems[gimmickId]
|
||||||
|
return id, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// importantItemTypeCageMemory mirrors EntityMImportantItem.ImportantItemType==4 — the
|
||||||
|
// CageMemory entry that the library's HasCageMemory check resolves to.
|
||||||
|
const importantItemTypeCageMemory int32 = 4
|
||||||
|
|
||||||
|
func loadCageMemoryImportantItems(typeByGimmick map[int32]model.GimmickType) map[int32]int32 {
|
||||||
|
empty := map[int32]int32{}
|
||||||
|
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
chapters, ok := readGimmickTable[EntityMMainQuestChapter]("m_main_quest_chapter", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
routes, ok := readGimmickTable[EntityMMainQuestRoute]("m_main_quest_route", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
cageMemories, ok := readGimmickTable[EntityMCageMemory]("m_cage_memory", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
items, ok := readGimmickTable[EntityMImportantItem]("m_important_item", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterByOrnamentGroup := make(map[int32]int32, len(ornaments))
|
||||||
|
for _, o := range ornaments {
|
||||||
|
if _, seen := chapterByOrnamentGroup[o.GimmickOrnamentGroupId]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chapterByOrnamentGroup[o.GimmickOrnamentGroupId] = o.ChapterId
|
||||||
|
}
|
||||||
|
routeByChapter := make(map[int32]int32, len(chapters))
|
||||||
|
for _, c := range chapters {
|
||||||
|
routeByChapter[c.MainQuestChapterId] = c.MainQuestRouteId
|
||||||
|
}
|
||||||
|
seasonByRoute := make(map[int32]int32, len(routes))
|
||||||
|
for _, r := range routes {
|
||||||
|
seasonByRoute[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||||
|
}
|
||||||
|
cmsBySeason := make(map[int32][]int32)
|
||||||
|
for _, c := range cageMemories {
|
||||||
|
cmsBySeason[c.MainQuestSeasonId] = append(cmsBySeason[c.MainQuestSeasonId], c.CageMemoryId)
|
||||||
|
}
|
||||||
|
for s := range cmsBySeason {
|
||||||
|
sort.Slice(cmsBySeason[s], func(i, j int) bool { return cmsBySeason[s][i] < cmsBySeason[s][j] })
|
||||||
|
}
|
||||||
|
itemByCageMemory := make(map[int32]int32)
|
||||||
|
for _, it := range items {
|
||||||
|
if it.ImportantItemType == importantItemTypeCageMemory && it.CageMemoryId != 0 {
|
||||||
|
itemByCageMemory[it.CageMemoryId] = it.ImportantItemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicksByRoute := make(map[int32][]int32)
|
||||||
|
for gid, t := range typeByGimmick {
|
||||||
|
if t != model.GimmickTypeCageMemory {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chapter, ok := chapterByOrnamentGroup[gid]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage-memory %d has no ornament row, skipping mapping", gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
route, ok := routeByChapter[chapter]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage-memory %d chapter %d has no route, skipping mapping", gid, chapter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gimmicksByRoute[route] = append(gimmicksByRoute[route], gid)
|
||||||
|
}
|
||||||
|
for r := range gimmicksByRoute {
|
||||||
|
sort.Slice(gimmicksByRoute[r], func(i, j int) bool { return gimmicksByRoute[r][i] < gimmicksByRoute[r][j] })
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[int32]int32)
|
||||||
|
for route, gids := range gimmicksByRoute {
|
||||||
|
season, ok := seasonByRoute[route]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] route %d has no season, skipping %d cage-memory gimmicks", route, len(gids))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seasonCms := cmsBySeason[season]
|
||||||
|
for i, gid := range gids {
|
||||||
|
if i >= len(seasonCms) {
|
||||||
|
log.Printf("[gimmick] route %d (season %d) has %d cage-memory gimmicks but only %d cage memories; gimmick %d skipped",
|
||||||
|
route, season, len(gids), len(seasonCms), gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cageMemoryId := seasonCms[i]
|
||||||
|
itemId, ok := itemByCageMemory[cageMemoryId]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage memory %d (gimmick %d) has no m_important_item row (type 4), skipping",
|
||||||
|
cageMemoryId, gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[gid] = itemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGimmickSequenceRewards() map[int32][]SequenceReward {
|
||||||
|
empty := map[int32][]SequenceReward{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "sequence rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
rewardGroups, ok := readGimmickTable[EntityMGimmickSequenceRewardGroup]("m_gimmick_sequence_reward_group", "sequence rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardsByGroup := make(map[int32][]SequenceReward)
|
||||||
|
for _, rg := range rewardGroups {
|
||||||
|
if rg.PossessionType == 0 || rg.PossessionId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rewardsByGroup[rg.GimmickSequenceRewardGroupId] = append(
|
||||||
|
rewardsByGroup[rg.GimmickSequenceRewardGroupId], SequenceReward{
|
||||||
|
PossessionType: rg.PossessionType,
|
||||||
|
PossessionId: rg.PossessionId,
|
||||||
|
Count: rg.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardsBySequence := make(map[int32][]SequenceReward, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
if rewards := rewardsByGroup[seq.GimmickSequenceRewardGroupId]; len(rewards) > 0 {
|
||||||
|
rewardsBySequence[seq.GimmickSequenceId] = rewards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rewardsBySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) IsHiddenSequence(sequenceId int32) bool {
|
||||||
|
return c.hiddenSequences[sequenceId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) SequenceRewards(sequenceId int32) []SequenceReward {
|
||||||
|
return c.sequenceRewards[sequenceId]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
|
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
|
||||||
var keys []store.GimmickSequenceKey
|
keys := make([]store.GimmickSequenceKey, 0, len(c.schedules))
|
||||||
for _, s := range c.schedules {
|
for _, s := range c.schedules {
|
||||||
if nowMillis < s.StartDatetime || nowMillis > s.EndDatetime {
|
if nowMillis < s.StartDatetime {
|
||||||
continue
|
continue // future schedules still skipped
|
||||||
}
|
}
|
||||||
if s.RequiredQuestId != 0 {
|
if !s.IsHidden && s.RequiredQuestId != 0 {
|
||||||
q, ok := user.Quests[s.RequiredQuestId]
|
q, ok := user.Quests[s.RequiredQuestId]
|
||||||
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
||||||
continue
|
continue
|
||||||
@@ -66,3 +472,126 @@ func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int6
|
|||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GimmickOrnamentRef struct {
|
||||||
|
GimmickId int32
|
||||||
|
OrnamentIndex int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickOrnamentRefs() map[int32][]GimmickOrnamentRef {
|
||||||
|
empty := map[int32][]GimmickOrnamentRef{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickGroup]("m_gimmick_group", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
indicesByOrnamentGroup := make(map[int32][]int32)
|
||||||
|
for _, o := range ornaments {
|
||||||
|
indicesByOrnamentGroup[o.GimmickOrnamentGroupId] = append(
|
||||||
|
indicesByOrnamentGroup[o.GimmickOrnamentGroupId], o.GimmickOrnamentIndex)
|
||||||
|
}
|
||||||
|
ornamentGroupByGimmick := make(map[int32]int32, len(gimmicks))
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
ornamentGroupByGimmick[g.GimmickId] = g.GimmickOrnamentGroupId
|
||||||
|
}
|
||||||
|
gimmicksByGroup := make(map[int32][]int32)
|
||||||
|
for _, grp := range groups {
|
||||||
|
gimmicksByGroup[grp.GimmickGroupId] = append(gimmicksByGroup[grp.GimmickGroupId], grp.GimmickId)
|
||||||
|
}
|
||||||
|
|
||||||
|
refsBySequence := make(map[int32][]GimmickOrnamentRef, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
var refs []GimmickOrnamentRef
|
||||||
|
for _, gimmickId := range gimmicksByGroup[seq.GimmickGroupId] {
|
||||||
|
for _, ornamentIndex := range indicesByOrnamentGroup[ornamentGroupByGimmick[gimmickId]] {
|
||||||
|
refs = append(refs, GimmickOrnamentRef{GimmickId: gimmickId, OrnamentIndex: ornamentIndex})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(refs) > 0 {
|
||||||
|
refsBySequence[seq.GimmickSequenceId] = refs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("gimmick ornament refs loaded: %d sequences", len(refsBySequence))
|
||||||
|
return refsBySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadHiddenGimmickSequenceIDs() map[int32]bool {
|
||||||
|
types := gimmickSequenceTypes()
|
||||||
|
out := make(map[int32]bool, len(types))
|
||||||
|
for sid, t := range types {
|
||||||
|
if t == model.GimmickTypeReport || t == model.GimmickTypeCageMemory || t == model.GimmickTypeMapOnlyCageTreasureHunt {
|
||||||
|
out[sid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadBirdGimmickIDs() map[int32]bool {
|
||||||
|
byGimmick := gimmickTypes().byGimmick
|
||||||
|
out := make(map[int32]bool, len(byGimmick))
|
||||||
|
for gid, t := range byGimmick {
|
||||||
|
if t == model.GimmickTypeCageIntervalDropItem || t == model.GimmickTypeMapOnlyCageIntervalDrop {
|
||||||
|
out[gid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickSequenceChains() map[int32][]int32 {
|
||||||
|
empty := map[int32][]int32{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "sequence chains")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickSequenceGroup]("m_gimmick_sequence_group", "sequence chains")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
membersByGroup := make(map[int32][]int32)
|
||||||
|
for _, g := range groups {
|
||||||
|
membersByGroup[g.GimmickSequenceGroupId] = append(membersByGroup[g.GimmickSequenceGroupId], g.GimmickSequenceId)
|
||||||
|
}
|
||||||
|
nextGroupBySequence := make(map[int32]int32, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
nextGroupBySequence[seq.GimmickSequenceId] = seq.NextGimmickSequenceGroupId
|
||||||
|
}
|
||||||
|
|
||||||
|
chains := make(map[int32][]int32, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
start := seq.GimmickSequenceId
|
||||||
|
seen := map[int32]bool{start: true}
|
||||||
|
chain := []int32{start}
|
||||||
|
for queue := []int32{start}; len(queue) > 0; {
|
||||||
|
cur := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
nextGroup := nextGroupBySequence[cur]
|
||||||
|
if nextGroup == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, member := range membersByGroup[nextGroup] {
|
||||||
|
if !seen[member] {
|
||||||
|
seen[member] = true
|
||||||
|
chain = append(chain, member)
|
||||||
|
queue = append(queue, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chains[start] = chain
|
||||||
|
}
|
||||||
|
return chains
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HiddenStoryRequirements struct {
|
||||||
|
MissionIds []int32
|
||||||
|
QuestMissions []store.QuestMissionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadHiddenStoryRequirements() HiddenStoryRequirements {
|
||||||
|
var empty HiddenStoryRequirements
|
||||||
|
|
||||||
|
gimmicks, err := utils.ReadTable[EntityMGimmick]("m_gimmick")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_gimmick unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
conditions, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_evaluate_condition unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
valueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_evaluate_condition_value_group unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
condById := make(map[int32]EntityMEvaluateCondition, len(conditions))
|
||||||
|
for _, c := range conditions {
|
||||||
|
condById[c.EvaluateConditionId] = c
|
||||||
|
}
|
||||||
|
valuesByGroup := make(map[int32]map[int32]int64)
|
||||||
|
for _, vg := range valueGroups {
|
||||||
|
g := valuesByGroup[vg.EvaluateConditionValueGroupId]
|
||||||
|
if g == nil {
|
||||||
|
g = make(map[int32]int64)
|
||||||
|
valuesByGroup[vg.EvaluateConditionValueGroupId] = g
|
||||||
|
}
|
||||||
|
g[vg.GroupIndex] = vg.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
missionSet := make(map[int32]struct{})
|
||||||
|
questMissionSet := make(map[store.QuestMissionKey]struct{})
|
||||||
|
seen := make(map[int32]bool)
|
||||||
|
|
||||||
|
var resolve func(conditionId int32, depth int)
|
||||||
|
resolve = func(conditionId int32, depth int) {
|
||||||
|
if conditionId == 0 || depth > 16 || seen[conditionId] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[conditionId] = true
|
||||||
|
c, ok := condById[conditionId]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group := valuesByGroup[c.EvaluateConditionValueGroupId]
|
||||||
|
switch model.EvaluateConditionFunctionType(c.EvaluateConditionFunctionType) {
|
||||||
|
case model.EvaluateConditionFunctionTypeRecursion:
|
||||||
|
// Value-group entries are sub-condition ids; satisfying all leaves makes
|
||||||
|
// both AND and OR recursion conditions evaluate true.
|
||||||
|
for _, sub := range group {
|
||||||
|
resolve(int32(sub), depth+1)
|
||||||
|
}
|
||||||
|
case model.EvaluateConditionFunctionTypeMissionClear:
|
||||||
|
if v, ok := group[defaultGroupIndex]; ok {
|
||||||
|
missionSet[int32(v)] = struct{}{}
|
||||||
|
}
|
||||||
|
case model.EvaluateConditionFunctionTypeQuestMissionClear:
|
||||||
|
questId, ok1 := group[1]
|
||||||
|
questMissionId, ok2 := group[2]
|
||||||
|
if ok1 && ok2 {
|
||||||
|
questMissionSet[store.QuestMissionKey{
|
||||||
|
QuestId: int32(questId),
|
||||||
|
QuestMissionId: int32(questMissionId),
|
||||||
|
}] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
switch model.GimmickType(g.GimmickType) {
|
||||||
|
case model.GimmickTypeReport, model.GimmickTypeCageMemory:
|
||||||
|
resolve(g.ClearEvaluateConditionId, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := HiddenStoryRequirements{}
|
||||||
|
for id := range missionSet {
|
||||||
|
req.MissionIds = append(req.MissionIds, id)
|
||||||
|
}
|
||||||
|
for key := range questMissionSet {
|
||||||
|
req.QuestMissions = append(req.QuestMissions, key)
|
||||||
|
}
|
||||||
|
log.Printf("hidden-story requirements: %d missions, %d quest-missions", len(req.MissionIds), len(req.QuestMissions))
|
||||||
|
return req
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type GimmickType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
GimmickTypeUnknown GimmickType = 0
|
||||||
|
GimmickTypeCageTreasureHunt GimmickType = 1 // "Fickle Black Birds" — in-cage flick-and-tap birds
|
||||||
|
GimmickTypeCageIntervalDropItem GimmickType = 2 // "Lost Items" — in-cage 3-dot drops that respawn on interval
|
||||||
|
GimmickTypeBrokenObelisk GimmickType = 3 // "Broken Scarecrow" (per tip text); zero rows in m_gimmick, unused
|
||||||
|
GimmickTypeIronGrill GimmickType = 4 // unused (zero rows in m_gimmick), in-game name unknown
|
||||||
|
GimmickTypeRadioMessage GimmickType = 5 // unused (zero rows in m_gimmick); client has GimmickRadioMessage class but no data
|
||||||
|
GimmickTypeFirstBrokenObelisk GimmickType = 6 // variant of Broken Scarecrow; zero rows in m_gimmick, unused
|
||||||
|
GimmickTypeMapOnlyCageTreasureHunt GimmickType = 7 // "Hidden Black Birds" — world-map birds; per-tap reward from m_cage_ornament_reward
|
||||||
|
GimmickTypeMapOnlyCageIntervalDrop GimmickType = 8 // map-side variant of Lost Items
|
||||||
|
GimmickTypeReport GimmickType = 9 // "Hidden Stories" — hidden mission markers
|
||||||
|
GimmickTypeCageMemory GimmickType = 10 // "Lost Archives" — collectible library entries (one-shot ImportantItem type-4)
|
||||||
|
GimmickTypeMapOnlyHideObelisk GimmickType = 11 // "Stray Scarecrow" — world-map scarecrows (not yet implemented)
|
||||||
|
)
|
||||||
@@ -43,6 +43,15 @@ const (
|
|||||||
QuestResultTypeFullResult QuestResultType = 3
|
QuestResultTypeFullResult QuestResultType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MissionProgressStatusType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
MissionProgressStatusTypeUnknown MissionProgressStatusType = 0
|
||||||
|
MissionProgressStatusTypeInProgress MissionProgressStatusType = 1
|
||||||
|
MissionProgressStatusTypeClear MissionProgressStatusType = 2
|
||||||
|
MissionProgressStatusTypeRewardReceived MissionProgressStatusType = 9
|
||||||
|
)
|
||||||
|
|
||||||
type QuestSceneType int32
|
type QuestSceneType int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
}
|
}
|
||||||
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
|
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
|
||||||
|
|
||||||
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
|
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver, cageOrnamentCatalog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load gimmick catalog: %w", err)
|
return nil, fmt.Errorf("load gimmick catalog: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
|
|||||||
|
|
||||||
cat := s.holder.Get()
|
cat := s.holder.Get()
|
||||||
reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId)
|
reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId)
|
||||||
if !ok {
|
|
||||||
log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId)
|
|
||||||
}
|
|
||||||
granter := cat.QuestHandler.Granter
|
|
||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
@@ -40,9 +36,21 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
|
|||||||
AcquisitionDatetime: nowMillis,
|
AcquisitionDatetime: nowMillis,
|
||||||
LatestVersion: nowMillis,
|
LatestVersion: nowMillis,
|
||||||
}
|
}
|
||||||
granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
if ok {
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
// "Fickle Black Birds" (type-1 gimmicks) tap into this RPC with CageOrnamentIds
|
||||||
|
// not present in m_cage_ornament_reward (their GimmickOrnamentViewIds are 101/103,
|
||||||
|
// not the 1002xxx-style ids the table uses). Record the access and return an empty
|
||||||
|
// reward so the client doesn't hang and the server doesn't crash.
|
||||||
|
log.Printf("[CageOrnamentService] ReceiveReward: no reward mapping for cageOrnamentId=%d, returning empty",
|
||||||
|
req.CageOrnamentId)
|
||||||
|
return &pb.ReceiveRewardResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.ReceiveRewardResponse{
|
return &pb.ReceiveRewardResponse{
|
||||||
CageOrnamentReward: []*pb.CageOrnamentReward{
|
CageOrnamentReward: []*pb.CageOrnamentReward{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/runtime"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
@@ -43,6 +45,10 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
log.Printf("[GimmickService] UpdateGimmickProgress: scheduleId=%d sequenceId=%d gimmickId=%d ornamentIndex=%d progressValueBit=%d flowType=%d",
|
log.Printf("[GimmickService] UpdateGimmickProgress: scheduleId=%d sequenceId=%d gimmickId=%d ornamentIndex=%d progressValueBit=%d flowType=%d",
|
||||||
req.GimmickSequenceScheduleId, req.GimmickSequenceId, req.GimmickId, req.GimmickOrnamentIndex, req.ProgressValueBit, req.FlowType)
|
req.GimmickSequenceScheduleId, req.GimmickSequenceId, req.GimmickId, req.GimmickOrnamentIndex, req.ProgressValueBit, req.FlowType)
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
cat := s.holder.Get()
|
||||||
|
|
||||||
|
var ornamentRewards []*pb.GimmickReward
|
||||||
|
var sequenceCleared bool
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
progressKey := store.GimmickKey{
|
progressKey := store.GimmickKey{
|
||||||
@@ -53,7 +59,6 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
progress := user.Gimmick.Progress[progressKey]
|
progress := user.Gimmick.Progress[progressKey]
|
||||||
progress.Key = progressKey
|
progress.Key = progressKey
|
||||||
progress.StartDatetime = nowMillis
|
progress.StartDatetime = nowMillis
|
||||||
user.Gimmick.Progress[progressKey] = progress
|
|
||||||
|
|
||||||
ornamentKey := store.GimmickOrnamentKey{
|
ornamentKey := store.GimmickOrnamentKey{
|
||||||
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
||||||
@@ -66,28 +71,151 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
ornament.ProgressValueBit = req.ProgressValueBit
|
ornament.ProgressValueBit = req.ProgressValueBit
|
||||||
ornament.BaseDatetime = nowMillis
|
ornament.BaseDatetime = nowMillis
|
||||||
user.Gimmick.OrnamentProgress[ornamentKey] = ornament
|
user.Gimmick.OrnamentProgress[ornamentKey] = ornament
|
||||||
|
|
||||||
|
// Per-type branches:
|
||||||
|
// * Report (type 9, "Hidden Stories") — mark gimmick + sequence
|
||||||
|
// cleared, grant SequenceRewards (ImportantItem type 3, library reads it).
|
||||||
|
// * MapOnlyCageTreasureHunt (type 7, "Hidden Black Birds") — same as Report
|
||||||
|
// but the per-tap reward also comes back from m_cage_ornament_reward via
|
||||||
|
// GimmickOrnamentViewId.
|
||||||
|
// * CageMemory (type 10, "Lost Archives") — resolve an ImportantItem
|
||||||
|
// (type 4) from the gimmick's monitor texture and grant it. IsGimmickCleared
|
||||||
|
// stays false (matches original userdata; only ornament progress flips).
|
||||||
|
// * CageTreasureHunt / CageIntervalDropItem* — stub per-tap material so
|
||||||
|
// the client's reward popup fires; real reward source still unmapped.
|
||||||
|
switch cat.Gimmick.GimmickType(req.GimmickId) {
|
||||||
|
case model.GimmickTypeReport:
|
||||||
|
progress.IsGimmickCleared = true
|
||||||
|
sequenceCleared = markSequenceClearedOnce(user, cat, req.GimmickSequenceScheduleId, req.GimmickSequenceId, nowMillis)
|
||||||
|
|
||||||
|
case model.GimmickTypeMapOnlyCageTreasureHunt:
|
||||||
|
r, ok := cat.Gimmick.HiddenBirdReward(req.GimmickId, req.GimmickOrnamentIndex)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[GimmickService] UpdateGimmickProgress: hidden-bird %d ornament %d has no reward mapping, skipping",
|
||||||
|
req.GimmickId, req.GimmickOrnamentIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(r.PossessionType), r.PossessionId, r.Count, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
progress.IsGimmickCleared = true
|
||||||
|
sequenceCleared = markSequenceClearedOnce(user, cat, req.GimmickSequenceScheduleId, req.GimmickSequenceId, nowMillis)
|
||||||
|
|
||||||
|
case model.GimmickTypeCageMemory:
|
||||||
|
itemId, ok := cat.Gimmick.CageMemoryImportantItem(req.GimmickId)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[GimmickService] UpdateGimmickProgress: cage memory %d has no important-item mapping, skipping grant",
|
||||||
|
req.GimmickId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, owned := user.ImportantItems[itemId]; owned {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionTypeImportantItem, itemId, 1, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: int32(model.PossessionTypeImportantItem),
|
||||||
|
PossessionId: itemId,
|
||||||
|
Count: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
case model.GimmickTypeCageTreasureHunt,
|
||||||
|
model.GimmickTypeCageIntervalDropItem,
|
||||||
|
model.GimmickTypeMapOnlyCageIntervalDrop:
|
||||||
|
// Per-tap drops with no per-gimmick reward in master data:
|
||||||
|
// * type 1 — "Fickle Black Birds" in the cage
|
||||||
|
// * type 2 — "Lost Items" in the cage
|
||||||
|
// * type 8 — Lost Items (map variant)
|
||||||
|
// Stub: grant 1 of Material 100004 (the most-common reward across
|
||||||
|
// m_cage_ornament_reward — 15 occurrences — likely a low-tier shard) per
|
||||||
|
// tap so the client's reward-popup path fires and the player accumulates
|
||||||
|
// something. Replace once a real per-gimmick mapping surfaces.
|
||||||
|
const stubMaterialId = int32(100004)
|
||||||
|
const stubMaterialCount = int32(1)
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionTypeMaterial, stubMaterialId, stubMaterialCount, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: int32(model.PossessionTypeMaterial),
|
||||||
|
PossessionId: stubMaterialId,
|
||||||
|
Count: stubMaterialCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
user.Gimmick.Progress[progressKey] = progress
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var clearReward []*pb.GimmickReward
|
||||||
|
if sequenceCleared {
|
||||||
|
for _, r := range cat.Gimmick.SequenceRewards(req.GimmickSequenceId) {
|
||||||
|
clearReward = append(clearReward, &pb.GimmickReward{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return &pb.UpdateGimmickProgressResponse{
|
return &pb.UpdateGimmickProgressResponse{
|
||||||
GimmickOrnamentReward: []*pb.GimmickReward{},
|
GimmickOrnamentReward: ornamentRewards,
|
||||||
IsSequenceCleared: false,
|
IsSequenceCleared: sequenceCleared,
|
||||||
GimmickSequenceClearReward: []*pb.GimmickReward{},
|
GimmickSequenceClearReward: clearReward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func markSequenceClearedOnce(user *store.UserState, cat *runtime.Catalogs, scheduleId, sequenceId int32, nowMillis int64) bool {
|
||||||
|
seqKey := store.GimmickSequenceKey{
|
||||||
|
GimmickSequenceScheduleId: scheduleId,
|
||||||
|
GimmickSequenceId: sequenceId,
|
||||||
|
}
|
||||||
|
sequence := user.Gimmick.Sequences[seqKey]
|
||||||
|
sequence.Key = seqKey
|
||||||
|
defer func() { user.Gimmick.Sequences[seqKey] = sequence }()
|
||||||
|
|
||||||
|
if sequence.IsGimmickSequenceCleared {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sequence.IsGimmickSequenceCleared = true
|
||||||
|
sequence.ClearDatetime = nowMillis
|
||||||
|
for _, r := range cat.Gimmick.SequenceRewards(sequenceId) {
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(r.PossessionType), r.PossessionId, r.Count, nowMillis)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) {
|
func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) {
|
||||||
log.Printf("[GimmickService] InitSequenceSchedule")
|
log.Printf("[GimmickService] InitSequenceSchedule")
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
eligible := s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now)
|
||||||
|
eligibleSet := make(map[store.GimmickSequenceKey]struct{}, len(eligible))
|
||||||
|
for _, key := range eligible {
|
||||||
|
eligibleSet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
pruned := 0
|
||||||
|
for key, entry := range user.Gimmick.Sequences {
|
||||||
|
if _, ok := eligibleSet[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.IsGimmickSequenceCleared {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(user.Gimmick.Sequences, key)
|
||||||
|
pruned++
|
||||||
|
}
|
||||||
|
|
||||||
added := 0
|
added := 0
|
||||||
for _, key := range s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now) {
|
for _, key := range eligible {
|
||||||
|
if len(user.Gimmick.Sequences) >= masterdata.MaxUserGimmickRows {
|
||||||
|
break
|
||||||
|
}
|
||||||
if _, exists := user.Gimmick.Sequences[key]; !exists {
|
if _, exists := user.Gimmick.Sequences[key]; !exists {
|
||||||
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
|
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if added > 0 {
|
if pruned > 0 || added > 0 {
|
||||||
log.Printf("[GimmickService] InitSequenceSchedule: added %d sequences (total %d)", added, len(user.Gimmick.Sequences))
|
log.Printf("[GimmickService] InitSequenceSchedule: pruned %d stale, added %d sequences (total %d, eligible %d, cap %d)",
|
||||||
|
pruned, added, len(user.Gimmick.Sequences), len(eligible), masterdata.MaxUserGimmickRows)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return &pb.InitSequenceScheduleResponse{}, nil
|
return &pb.InitSequenceScheduleResponse{}, nil
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const (
|
|||||||
starterMissionId = int32(1)
|
starterMissionId = int32(1)
|
||||||
starterMainQuestRouteId = int32(1)
|
starterMainQuestRouteId = int32(1)
|
||||||
starterMainQuestSeasonId = int32(1)
|
starterMainQuestSeasonId = int32(1)
|
||||||
missionInProgress = int32(1)
|
|
||||||
|
|
||||||
defaultBirthYear = int32(2000)
|
defaultBirthYear = int32(2000)
|
||||||
defaultBirthMonth = int32(1)
|
defaultBirthMonth = int32(1)
|
||||||
@@ -114,7 +113,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
|
|||||||
starterMissionId: {
|
starterMissionId: {
|
||||||
MissionId: starterMissionId,
|
MissionId: starterMissionId,
|
||||||
StartDatetime: nowMillis,
|
StartDatetime: nowMillis,
|
||||||
MissionProgressStatusType: missionInProgress,
|
MissionProgressStatusType: int32(model.MissionProgressStatusTypeInProgress),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Gimmick: GimmickState{
|
Gimmick: GimmickState{
|
||||||
|
|||||||
@@ -287,10 +287,12 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !gimmickStateEqual(before.Gimmick, after.Gimmick) {
|
if !gimmickStateEqual(before.Gimmick, after.Gimmick) {
|
||||||
if !mapsEqualStruct(before.Gimmick.Progress, after.Gimmick.Progress) {
|
if !mapsEqualStruct(before.Gimmick.Progress, after.Gimmick.Progress) ||
|
||||||
|
!mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
add("IUserGimmick")
|
add("IUserGimmick")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Gimmick.OrnamentProgress, after.Gimmick.OrnamentProgress) {
|
if !mapsEqualStruct(before.Gimmick.OrnamentProgress, after.Gimmick.OrnamentProgress) ||
|
||||||
|
!mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
add("IUserGimmickOrnamentProgress")
|
add("IUserGimmickOrnamentProgress")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
if !mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ package userdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var gimmickOrnamentRefs = sync.OnceValue(masterdata.LoadGimmickOrnamentRefs)
|
||||||
|
var gimmickSequenceChains = sync.OnceValue(masterdata.LoadGimmickSequenceChains)
|
||||||
|
var hiddenSequenceSet = sync.OnceValue(masterdata.LoadHiddenGimmickSequenceIDs)
|
||||||
|
var gimmickSequenceRanks = sync.OnceValue(masterdata.LoadGimmickSequenceRanks)
|
||||||
|
var birdGimmicks = sync.OnceValue(masterdata.LoadBirdGimmickIDs)
|
||||||
|
|
||||||
|
const birdDefaultBaseDatetime int64 = 1577836800000 // 2020-01-01 00:00:00 UTC in ms
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
register("IUserGimmick", func(user store.UserState) string {
|
register("IUserGimmick", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(sortedGimmickRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedGimmickRecords(user)...)
|
||||||
@@ -26,9 +36,65 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func projectActiveChainOrnaments(
|
||||||
|
user store.UserState,
|
||||||
|
addKey func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef),
|
||||||
|
sizeFn func() int,
|
||||||
|
cap int,
|
||||||
|
) {
|
||||||
|
refs := gimmickOrnamentRefs()
|
||||||
|
chains := gimmickSequenceChains()
|
||||||
|
hiddenSeq := hiddenSequenceSet()
|
||||||
|
|
||||||
|
walkChain := func(seqKey store.GimmickSequenceKey) {
|
||||||
|
chain := chains[seqKey.GimmickSequenceId]
|
||||||
|
if len(chain) == 0 {
|
||||||
|
chain = []int32{seqKey.GimmickSequenceId}
|
||||||
|
}
|
||||||
|
for _, seqId := range chain {
|
||||||
|
for _, ref := range refs[seqId] {
|
||||||
|
addKey(seqKey, seqId, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonHidden []store.GimmickSequenceKey
|
||||||
|
for seqKey := range user.Gimmick.Sequences {
|
||||||
|
if hiddenSeq[seqKey.GimmickSequenceId] {
|
||||||
|
walkChain(seqKey)
|
||||||
|
} else {
|
||||||
|
nonHidden = append(nonHidden, seqKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, seqKey := range nonHidden {
|
||||||
|
if sizeFn() >= cap {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
walkChain(seqKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sortedGimmickRecords(user store.UserState) []map[string]any {
|
func sortedGimmickRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.GimmickKey, 0, len(user.Gimmick.Progress))
|
|
||||||
|
keySet := make(map[store.GimmickKey]struct{})
|
||||||
|
// Real progress rows (genuine user data) — always kept.
|
||||||
for key := range user.Gimmick.Progress {
|
for key := range user.Gimmick.Progress {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
projectActiveChainOrnaments(user,
|
||||||
|
func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef) {
|
||||||
|
keySet[store.GimmickKey{
|
||||||
|
GimmickSequenceScheduleId: seqKey.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId: seqId,
|
||||||
|
GimmickId: ref.GimmickId,
|
||||||
|
}] = struct{}{}
|
||||||
|
},
|
||||||
|
func() int { return len(keySet) },
|
||||||
|
masterdata.MaxUserGimmickRows,
|
||||||
|
)
|
||||||
|
|
||||||
|
keys := make([]store.GimmickKey, 0, len(keySet))
|
||||||
|
for key := range keySet {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
@@ -37,57 +103,103 @@ func sortedGimmickRecords(user store.UserState) []map[string]any {
|
|||||||
|
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
row := user.Gimmick.Progress[key]
|
isGimmickCleared := false
|
||||||
|
startDatetime := user.GameStartDatetime
|
||||||
|
latestVersion := user.GameStartDatetime
|
||||||
|
if row, ok := user.Gimmick.Progress[key]; ok {
|
||||||
|
isGimmickCleared = row.IsGimmickCleared
|
||||||
|
startDatetime = row.StartDatetime
|
||||||
|
latestVersion = row.LatestVersion
|
||||||
|
}
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
|
"gimmickSequenceScheduleId": key.GimmickSequenceScheduleId,
|
||||||
"gimmickSequenceId": row.Key.GimmickSequenceId,
|
"gimmickSequenceId": key.GimmickSequenceId,
|
||||||
"gimmickId": row.Key.GimmickId,
|
"gimmickId": key.GimmickId,
|
||||||
"isGimmickCleared": row.IsGimmickCleared,
|
"isGimmickCleared": isGimmickCleared,
|
||||||
"startDatetime": row.StartDatetime,
|
"startDatetime": startDatetime,
|
||||||
"latestVersion": row.LatestVersion,
|
"latestVersion": latestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedGimmickOrnamentProgressRecords(user store.UserState) []map[string]any {
|
func sortedGimmickOrnamentProgressRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.GimmickOrnamentKey, 0, len(user.Gimmick.OrnamentProgress))
|
|
||||||
|
keySet := make(map[store.GimmickOrnamentKey]struct{})
|
||||||
|
// Real progress rows (genuine user data) — always kept.
|
||||||
for key := range user.Gimmick.OrnamentProgress {
|
for key := range user.Gimmick.OrnamentProgress {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
projectActiveChainOrnaments(user,
|
||||||
|
func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef) {
|
||||||
|
keySet[store.GimmickOrnamentKey{
|
||||||
|
GimmickSequenceScheduleId: seqKey.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId: seqId,
|
||||||
|
GimmickId: ref.GimmickId,
|
||||||
|
GimmickOrnamentIndex: ref.OrnamentIndex,
|
||||||
|
}] = struct{}{}
|
||||||
|
},
|
||||||
|
func() int { return len(keySet) },
|
||||||
|
masterdata.MaxUserGimmickRows,
|
||||||
|
)
|
||||||
|
|
||||||
|
keys := make([]store.GimmickOrnamentKey, 0, len(keySet))
|
||||||
|
for key := range keySet {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
return compareGimmickOrnamentKey(keys[i], keys[j]) < 0
|
return compareGimmickOrnamentKey(keys[i], keys[j]) < 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
birdG := birdGimmicks()
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
row := user.Gimmick.OrnamentProgress[key]
|
progressValueBit := int32(0)
|
||||||
|
baseDatetime := user.GameStartDatetime
|
||||||
|
latestVersion := user.GameStartDatetime
|
||||||
|
if row, ok := user.Gimmick.OrnamentProgress[key]; ok {
|
||||||
|
progressValueBit = row.ProgressValueBit
|
||||||
|
baseDatetime = row.BaseDatetime
|
||||||
|
latestVersion = row.LatestVersion
|
||||||
|
} else if birdG[key.GimmickId] {
|
||||||
|
baseDatetime = birdDefaultBaseDatetime
|
||||||
|
}
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
|
"gimmickSequenceScheduleId": key.GimmickSequenceScheduleId,
|
||||||
"gimmickSequenceId": row.Key.GimmickSequenceId,
|
"gimmickSequenceId": key.GimmickSequenceId,
|
||||||
"gimmickId": row.Key.GimmickId,
|
"gimmickId": key.GimmickId,
|
||||||
"gimmickOrnamentIndex": row.Key.GimmickOrnamentIndex,
|
"gimmickOrnamentIndex": key.GimmickOrnamentIndex,
|
||||||
"progressValueBit": row.ProgressValueBit,
|
"progressValueBit": progressValueBit,
|
||||||
"baseDatetime": row.BaseDatetime,
|
"baseDatetime": baseDatetime,
|
||||||
"latestVersion": row.LatestVersion,
|
"latestVersion": latestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedGimmickSequenceRecords(user store.UserState) []map[string]any {
|
func sortedGimmickSequenceRecords(user store.UserState) []map[string]any {
|
||||||
|
|
||||||
|
ranks := gimmickSequenceRanks()
|
||||||
|
|
||||||
keys := make([]store.GimmickSequenceKey, 0, len(user.Gimmick.Sequences))
|
keys := make([]store.GimmickSequenceKey, 0, len(user.Gimmick.Sequences))
|
||||||
for key := range user.Gimmick.Sequences {
|
for key := range user.Gimmick.Sequences {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
ri, rj := ranks[keys[i].GimmickSequenceId], ranks[keys[j].GimmickSequenceId]
|
||||||
|
if ri != rj {
|
||||||
|
return ri < rj
|
||||||
|
}
|
||||||
if keys[i].GimmickSequenceScheduleId != keys[j].GimmickSequenceScheduleId {
|
if keys[i].GimmickSequenceScheduleId != keys[j].GimmickSequenceScheduleId {
|
||||||
return keys[i].GimmickSequenceScheduleId < keys[j].GimmickSequenceScheduleId
|
return keys[i].GimmickSequenceScheduleId < keys[j].GimmickSequenceScheduleId
|
||||||
}
|
}
|
||||||
return keys[i].GimmickSequenceId < keys[j].GimmickSequenceId
|
return keys[i].GimmickSequenceId < keys[j].GimmickSequenceId
|
||||||
})
|
})
|
||||||
|
if len(keys) > masterdata.MaxUserGimmickRows {
|
||||||
|
keys = keys[:masterdata.MaxUserGimmickRows]
|
||||||
|
}
|
||||||
|
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
|
|||||||
@@ -33,8 +33,26 @@ func sortedQuestRecords(user store.UserState) []map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sortedQuestMissionRecords(user store.UserState) []map[string]any {
|
func sortedQuestMissionRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.QuestMissionKey, 0, len(user.QuestMissions))
|
questMissions := make(map[store.QuestMissionKey]store.UserQuestMissionState, len(user.QuestMissions))
|
||||||
for key := range user.QuestMissions {
|
for key, qm := range user.QuestMissions {
|
||||||
|
questMissions[key] = qm
|
||||||
|
}
|
||||||
|
// Force-clear hidden-story quest-missions so their report gimmicks unlock.
|
||||||
|
for _, key := range hiddenStoryRequirements().QuestMissions {
|
||||||
|
if existing, ok := questMissions[key]; ok && existing.IsClear {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
questMissions[key] = store.UserQuestMissionState{
|
||||||
|
QuestId: key.QuestId,
|
||||||
|
QuestMissionId: key.QuestMissionId,
|
||||||
|
IsClear: true,
|
||||||
|
LatestClearDatetime: user.GameStartDatetime,
|
||||||
|
LatestVersion: user.GameStartDatetime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]store.QuestMissionKey, 0, len(questMissions))
|
||||||
|
for key := range questMissions {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
@@ -45,7 +63,7 @@ func sortedQuestMissionRecords(user store.UserState) []map[string]any {
|
|||||||
})
|
})
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
row := user.QuestMissions[key]
|
row := questMissions[key]
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"questId": row.QuestId,
|
"questId": row.QuestId,
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package userdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
@@ -192,16 +195,35 @@ func sortedTutorialRecords(user store.UserState) []map[string]any {
|
|||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hiddenStoryRequirements = sync.OnceValue(masterdata.LoadHiddenStoryRequirements)
|
||||||
|
|
||||||
func sortedMissionRecords(user store.UserState) []map[string]any {
|
func sortedMissionRecords(user store.UserState) []map[string]any {
|
||||||
ids := make([]int, 0, len(user.Missions))
|
missions := make(map[int32]store.UserMissionState, len(user.Missions))
|
||||||
for id := range user.Missions {
|
for id, m := range user.Missions {
|
||||||
|
missions[id] = m
|
||||||
|
}
|
||||||
|
for _, missionId := range hiddenStoryRequirements().MissionIds {
|
||||||
|
if existing, ok := missions[missionId]; ok && existing.MissionProgressStatusType >= int32(model.MissionProgressStatusTypeClear) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missions[missionId] = store.UserMissionState{
|
||||||
|
MissionId: missionId,
|
||||||
|
StartDatetime: user.GameStartDatetime,
|
||||||
|
MissionProgressStatusType: int32(model.MissionProgressStatusTypeClear),
|
||||||
|
ClearDatetime: user.GameStartDatetime,
|
||||||
|
LatestVersion: user.GameStartDatetime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]int, 0, len(missions))
|
||||||
|
for id := range missions {
|
||||||
ids = append(ids, int(id))
|
ids = append(ids, int(id))
|
||||||
}
|
}
|
||||||
sort.Ints(ids)
|
sort.Ints(ids)
|
||||||
|
|
||||||
records := make([]map[string]any, 0, len(ids))
|
records := make([]map[string]any, 0, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
row := user.Missions[int32(id)]
|
row := missions[int32(id)]
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"missionId": row.MissionId,
|
"missionId": row.MissionId,
|
||||||
|
|||||||
Reference in New Issue
Block a user