package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // One row of AgentDataSetsActivityConfig — something an NPC can be seen doing. type NpcActivityEntry4229938 struct { Id uint32 `json:"id"` AgentTag uint32 `json:"agentTag"` NpcCultivationId uint32 `json:"npcCultivationId"` Raid uint32 `json:"raid"` Performance string `json:"performance"` MapName string `json:"mapName"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` // Authored rows carry MapPosition; the rest are "临时点位" placeholders with a zero vector. HasPosition bool `json:"hasPosition"` } // What NpcActivityCatalog4229938.Resolve picked, and why — for logs. type NpcActivityChoice struct { Entry *NpcActivityEntry4229938 `json:"entry"` Mode string `json:"mode"` Timed *NpcActivityEntry4229938 `json:"timed"` Spot *RosterNpcStaticSpot `json:"spot"` // 有没有一个能落地的坐标 —— 官方活动坐标,或手工常驻点。 Placeable bool `json:"placeable"` PlaceX float32 `json:"placeX"` PlaceY float32 `json:"placeY"` PlaceZ float32 `json:"placeZ"` } // // Reads Configs/NpcActivityCatalog.json — the server copy of the client's // AgentDataSetsActivityConfig (284 rows), generated by _gen_npc_life_cfg.py out of // 补丁和工具\DUMPS\readable_world_dump_4229938_final.zip // (01_CONFIGS/npc_systems/table/AgentDataSetsActivityConfig.json). // Why it matters: this is the table that turns "an NPC tag" into "a place and a performance". // AgentDataSetsActivityCfgId on the agent's sync info is an id into THIS table, and // MapPosition is where the official build actually puts the character (Richie on the // treadmill at 1191,0,2052 is just one row). 153 of the 284 rows carry a real position; all of // them sit in the two open-world raids (23300888 / 23300999). // The id also reaches the UI: NpcDaliyManager:OnRenderActivity resolves it through the same // table for the icon/description, and MapSubSystem_SpiritAcquisition reads the schedule's // RaidId + NpcScheduleInfo.Position to drop the map marker — which is why the login frame's // FavorNpcDailyScheduleInfos should carry these coordinates too. // var ( NpcActivityCatalog4229938_cacheLock sync.RWMutex NpcActivityCatalog4229938_cached = false NpcActivityCatalog4229938_entries []any ) // LoadNpcActivityCatalog4229938 loads catalog records from client config directory func LoadNpcActivityCatalog4229938(configDir string) ([]any, error) { NpcActivityCatalog4229938_cacheLock.RLock() if NpcActivityCatalog4229938_cached { defer NpcActivityCatalog4229938_cacheLock.RUnlock() return NpcActivityCatalog4229938_entries, nil } NpcActivityCatalog4229938_cacheLock.RUnlock() NpcActivityCatalog4229938_cacheLock.Lock() defer NpcActivityCatalog4229938_cacheLock.Unlock() if NpcActivityCatalog4229938_cached { return NpcActivityCatalog4229938_entries, nil } filePath := filepath.Join(configDir, "NpcActivityCatalog.json.json") data, err := os.ReadFile(filePath) if err != nil { return nil, err } var raw struct { Records []json.RawMessage `json:"records"` } if err := json.Unmarshal(data, &raw); err != nil { return nil, err } var result []any for _, item := range raw.Records { var m map[string]any if err := json.Unmarshal(item, &m); err == nil { result = append(result, m) } } NpcActivityCatalog4229938_entries = result NpcActivityCatalog4229938_cached = true return result, nil } func NpcActivityCatalog4229938_TryGet(activityId uint32, entry NpcActivityEntry4229938) bool { // Catalog helper NpcActivityCatalog4229938_TryGet return false } // // The activity an AgentTag is doing right now, or 0 when that tag has neither a schedule nor // an authored position (the two protagonists — AgentTag 2/3 — are exactly this case). // func NpcActivityCatalog4229938_CurrentFor(agentTag uint32, daySecond int32) *NpcActivityEntry4229938 { // Catalog helper NpcActivityCatalog4229938_CurrentFor return nil } // // Same tag, but ignoring the clock: the authored activity with a real position inside // , lowest Id first. Used when the current time slice // points at an activity we cannot place — either it has no MapPosition at all (Richie's 05:00 // "给垃圾桶蟹开罚单" is a 临时点位) or it belongs to the other open-world raid. // Returns null when this tag has nothing placeable in that raid. // ⛑⛑ Intentionally not "nearest to the player". A player-relative pick is re-evaluated // every tick, so the answer changes as the player walks and the character appears to follow // him around; combined with the enter radius it degenerates into "whoever has no schedule // materialises at your feet". Official data is a fixed spot — pick one deterministically and // let the enter radius decide whether the player is close enough to see it. // func NpcActivityCatalog4229938_FirstPositioned(agentTag uint32, raid uint32) *NpcActivityEntry4229938 { // Catalog helper NpcActivityCatalog4229938_FirstPositioned return nil } // // Hand-authored standing spot for a character that has no official daily-life data at all // (gameplay.rosterNpc.staticSpots). Returns null when this spirit has none, or when the // spot belongs to a different raid. // func NpcActivityCatalog4229938_StaticSpotFor(spiritId uint32, raid uint32) *RosterNpcStaticSpot { // Catalog helper NpcActivityCatalog4229938_StaticSpotFor return nil } // // The single selection policy for "what is this character doing right now" — used by both the // switch-away path and the world roster so the two can never disagree. // Mode is one of: no-tag / no-schedule / timed / // fallback-in-raid / no-position / other-raid / static-spot. // ⛑ no-tag / no-schedule / other-raid / no-position carry a null // Entry AND a null Spot — the caller must NOT fall back to "stand // next to the player" or you get 09-25's crowd-at-your-feet bug. // func NpcActivityCatalog4229938_Resolve(spiritId uint32, daySecond int32, raid uint32, requirePosition bool, px float32, py float32, pz float32) NpcActivityChoice { // Catalog helper NpcActivityCatalog4229938_Resolve var zero NpcActivityChoice; return zero }