mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 13:53: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 (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"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 {
|
||||
ScheduleId int32
|
||||
StartDatetime int64
|
||||
EndDatetime int64
|
||||
FirstSequenceId int32
|
||||
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 {
|
||||
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")
|
||||
if err != nil {
|
||||
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 {
|
||||
entry := gimmickScheduleEntry{
|
||||
ScheduleId: r.GimmickSequenceScheduleId,
|
||||
StartDatetime: r.StartDatetime,
|
||||
EndDatetime: r.EndDatetime,
|
||||
FirstSequenceId: r.FirstGimmickSequenceId,
|
||||
IsHidden: hiddenSeq[r.FirstGimmickSequenceId],
|
||||
Rank: gimmickTypeRank(seqTypes[r.FirstGimmickSequenceId]),
|
||||
}
|
||||
if r.ReleaseEvaluateConditionId != 0 {
|
||||
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
||||
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))
|
||||
return &GimmickCatalog{schedules: entries}, nil
|
||||
entries := make([]gimmickScheduleEntry, 0, len(bestBySeq))
|
||||
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 {
|
||||
var keys []store.GimmickSequenceKey
|
||||
keys := make([]store.GimmickSequenceKey, 0, len(c.schedules))
|
||||
for _, s := range c.schedules {
|
||||
if nowMillis < s.StartDatetime || nowMillis > s.EndDatetime {
|
||||
continue
|
||||
if nowMillis < s.StartDatetime {
|
||||
continue // future schedules still skipped
|
||||
}
|
||||
if s.RequiredQuestId != 0 {
|
||||
if !s.IsHidden && s.RequiredQuestId != 0 {
|
||||
q, ok := user.Quests[s.RequiredQuestId]
|
||||
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
||||
continue
|
||||
@@ -66,3 +472,126 @@ func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int6
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user