package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // // PartyConfig.json 里的**派对场地**信息(2026-09-26 第 34 轮)。 // WHY THIS FILE EXISTS // -------------------- // 第 33 轮把「邀请名单」修对之后,用户提出:NPC 不该被塞在玩家身边 2.5 m —— 那是我自己编的 // 站位(「取巧」);**官方在配表里给了规定位置** = `PartySpot` + `PartyRotation`。 // 逐字段核对(ConfigDump_v3\PartyConfig.json,共 7 条): // // 13030000 露营(新启) PartySpot=[1288.91003,-0.0900000036,2374.46997] PartyRotation=-76.6750031 MaxNum=3 RaidId=23300888 // 13030002 露营引导 PartySpot=[1276.94995,-0.295366287,2382.59009] PartyRotation=103.629997 // 13030003 舞会 PartySpot=[2574.23999,80.1350021,1271] PartyRotation=91.8000031(另有 DanceSpot/DanceRotation) // 13030004/13030005 联机 PartySpot=[0,0,0] ← ★「本行没有场地」的哨兵值,必须判掉 // // ★ `PartySpot` 在客户端两侧都**没有业务消费者**(Lua 侧只有 `ClientConfig.lua` 的字段注册; // C# metadata 侧只有 `get_PartySpot` / `set_PartySpot` / `<PartySpot>k__BackingField` / // `PartySpotIndex` 这类**配表访问器**) // ⇒ 它是**关卡/玩法侧**的场地标记,语义 = 「派对在这里举行」。 // 把它用作**服务端生成 NPC 的落点**,是这个语义最直接的用法,而且**完全由配表驱动** // (不是我们另编一个坐标)—— 这正是用户要的「把选定的角色放在规定的位置」。 // ⚠ 与服务端其它 catalog 的差异:**本表允许缺失**。 // `DonorNpcCatalog4229938` 在文件缺失时直接抛(它管的是核心 NPC 数据,缺了没得玩); // 而派对场地只是一个「比玩家身边更正统的落点」—— 缺表时应回退玩家锚点, // **不能让整个派对功能挂掉**。所以 Load() 全程 try/catch + 返回空表。 // ★ 第 38 轮新增 PartySpotEntry4229938.GamePlayType(配表字段 `GamePlayType`): // 它把 partyId 映射到 `NpcCultivationGameplayTypeConfig` 的 Id // (13030000/13030002 → 91055000 露营、13030003 → 91056002 舞会), // 调用方拿它给这批 NPC 选「预制动作」表(见 GameplaySettings.PartyNpcPoi*)。 // ⚠ 它**独立于** HasUsableSpot —— 联机那两条也有 GamePlayType,只是没有场地。 // type PartySpotEntry4229938 struct { Id uint32 `json:"id"` RaidId uint32 `json:"raidId"` MaxNum uint32 `json:"maxNum"` MaxDuration uint32 `json:"maxDuration"` GamePlayType uint32 `json:"gamePlayType"` Spot Vec3 `json:"spot"` Rotation float32 `json:"rotation"` // // 配表是否给了**可用**的站位。联机那两条(13030004 / 13030005)写的是 [0,0,0], // 那是「本行没有场地」的哨兵、不是真的在原点 ⇒ 判掉,让调用方回退。 // HasUsableSpot bool `json:"hasUsableSpot"` } var ( PartySpotCatalog4229938_cacheLock sync.RWMutex PartySpotCatalog4229938_cached = false PartySpotCatalog4229938_entries []any ) // LoadPartySpotCatalog4229938 loads catalog records from client config directory func LoadPartySpotCatalog4229938(configDir string) ([]any, error) { PartySpotCatalog4229938_cacheLock.RLock() if PartySpotCatalog4229938_cached { defer PartySpotCatalog4229938_cacheLock.RUnlock() return PartySpotCatalog4229938_entries, nil } PartySpotCatalog4229938_cacheLock.RUnlock() PartySpotCatalog4229938_cacheLock.Lock() defer PartySpotCatalog4229938_cacheLock.Unlock() if PartySpotCatalog4229938_cached { return PartySpotCatalog4229938_entries, nil } filePath := filepath.Join(configDir, "PartySpotCatalog.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) } } PartySpotCatalog4229938_entries = result PartySpotCatalog4229938_cached = true return result, nil } func PartySpotCatalog4229938_TryGet(partyId uint32, entry PartySpotEntry4229938) bool { // Catalog helper PartySpotCatalog4229938_TryGet return false } func PartySpotCatalog4229938_U32(row any, name string) uint32 { // Catalog helper PartySpotCatalog4229938_U32 return 0 } func PartySpotCatalog4229938_Single(row any, name string) float32 { // Catalog helper PartySpotCatalog4229938_Single return 0 } // 读 [x,y,z];形状不对或含 NaN 就返回零向量(⇒ HasUsableSpot = false)。 func PartySpotCatalog4229938_ReadVector(row any, name string) Vec3 { // Catalog helper PartySpotCatalog4229938_ReadVector var zero Vec3; return zero }