package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // One published message instance: the row itself plus the linear follow-up, both bare ids. type NpcChatNode4229938 struct { Id uint32 `json:"id"` NextId uint32 `json:"nextId"` } // // One conversation thread inside an owner's NpcChatListDict. // Key is the dictionary key the login frame must use — see NpcChatCatalog4229938 // for why it mirrors NPCChatConfig.AsNpcCultivation. Type is the head row's // ChatType (dump.cs:1326930 Normal=1 / Invite=2 / Dialog=3 / Fake=4) and is what lets a build // decide whether story-triggered threads ride along in the login frame. // type NpcChatThread4229938 struct { Key uint32 `json:"key"` Type uint32 `json:"type"` Items []NpcChatNode4229938 `json:"items"` } // One entry of either NpcChats (single NPC) or NpcGroupChats (group chat). type NpcChatOwner4229938 struct { Id uint32 `json:"id"` Members []uint32 `json:"members"` Threads []NpcChatThread4229938 `json:"threads"` } // // Reads Configs/NpcChats.json — a projection of the client's own NPCChatConfig (6215 rows) // and NPCChatGroupConfig (45 rows), built by _gen_npc_chats.py. This is what fills the 手机 // app BBChat (AppId 27 "Message" / SystemId 146). // Why the login frame: there is no "pull the list" RPC for it. PlayerInfoMinorNpcCultivationData.lua:55 // copies InfoMinor.InfoNpcCultivation.NpcChats / .NpcGroupChats straight into the player // data, NpcChatManager_Story.lua:28 converts them into m_npcChatsDict, and every list the // app shows is a projection of that table. Nothing else ever refills it, so leaving those two lists // empty is why the app had no conversations at all. // The server publishes only the instance — which row is unlocked, on what thread, in what order. Text, // speaker side, avatars and option labels come from the client's tables: // NpcChatUtils.NewFromNpcChatItem resolves everything from LTConfig.NPCChatConfig.GetConfig(chatItem.ChatId), // so an id that is not in NPCChatConfig is dropped with a "NPCChat表没有查到" error. // Nodes keeps the whole NextMessage map (every row, not just the published ones) so the // interact handlers can continue a conversation on rows the player reached themselves and that are // therefore not part of any published thread. // A missing or malformed file leaves the catalogue empty and LoadError explains why. // var ( NpcChatCatalog4229938_cacheLock sync.RWMutex NpcChatCatalog4229938_cached = false NpcChatCatalog4229938_entries []any ) // LoadNpcChatCatalog4229938 loads catalog records from client config directory func LoadNpcChatCatalog4229938(configDir string) ([]any, error) { NpcChatCatalog4229938_cacheLock.RLock() if NpcChatCatalog4229938_cached { defer NpcChatCatalog4229938_cacheLock.RUnlock() return NpcChatCatalog4229938_entries, nil } NpcChatCatalog4229938_cacheLock.RUnlock() NpcChatCatalog4229938_cacheLock.Lock() defer NpcChatCatalog4229938_cacheLock.Unlock() if NpcChatCatalog4229938_cached { return NpcChatCatalog4229938_entries, nil } filePath := filepath.Join(configDir, "NpcChatCatalog.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) } } NpcChatCatalog4229938_entries = result NpcChatCatalog4229938_cached = true return result, nil } // // Linear follow-up of any row, or 0 at the end of a conversation. NPCChatConfig.NextMessage // can branch, index 0 is the continuation the client also uses for "点一下继续". // func NpcChatCatalog4229938_NextOf(chatId uint32) uint32 { // Catalog helper NpcChatCatalog4229938_NextOf return 0 } func NpcChatCatalog4229938_Knows(chatId uint32) bool { // Catalog helper NpcChatCatalog4229938_Knows return false } // // Every member id of a group, taken from NPCChatGroupConfig.GroupMember. Used only to give // ClientNpcGroupChatData.Members something real — the client already knows who is in a group // from its own table, so this is belt and braces for a hand-edited catalogue. // func NpcChatCatalog4229938_MembersOf(groupId uint32) []uint32 { // Catalog helper NpcChatCatalog4229938_MembersOf return nil } func NpcChatCatalog4229938_ThreadCountOf(npcs []NpcChatOwner4229938, groups []NpcChatOwner4229938) int32 { // Catalog helper NpcChatCatalog4229938_ThreadCountOf return 0 } func NpcChatCatalog4229938_Owners(row any, nodes map[uint32][]uint32) *NpcChatOwner4229938 { // Catalog helper NpcChatCatalog4229938_Owners return nil } func NpcChatCatalog4229938_U32(source any, name string) uint32 { // Catalog helper NpcChatCatalog4229938_U32 return 0 }