mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 13:53:41 +03:00
598 lines
19 KiB
Go
598 lines
19 KiB
Go
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
|
|
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, 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)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
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 {
|
|
keys := make([]store.GimmickSequenceKey, 0, len(c.schedules))
|
|
for _, s := range c.schedules {
|
|
if nowMillis < s.StartDatetime {
|
|
continue // future schedules still skipped
|
|
}
|
|
if !s.IsHidden && s.RequiredQuestId != 0 {
|
|
q, ok := user.Quests[s.RequiredQuestId]
|
|
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
|
continue
|
|
}
|
|
}
|
|
keys = append(keys, store.GimmickSequenceKey{
|
|
GimmickSequenceScheduleId: s.ScheduleId,
|
|
GimmickSequenceId: s.FirstSequenceId,
|
|
})
|
|
}
|
|
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
|
|
}
|