package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // One prop the server restocks: a pathId, a world placement and its blob uid. type PropRestockItem4229938 struct { PathId int32 `json:"pathId"` Name string `json:"name"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` Yaw float32 `json:"yaw"` UniqueId uint64 `json:"uniqueId"` } // // All props captured from the client's own LX6.Item.GadgetBlob that belong to one // 40 m world cell of one map. // // // Map this cell belongs to: 23300888 = 新启, 23300999 = 重霄·凌云. Cell indices are only // unique within a raid — two maps reuse the same (cx,cz), so a raid-less table would // stock map B's props while the player stands in map A. // type PropRestockCell4229938 struct { Raid uint32 `json:"raid"` Cx int32 `json:"cx"` Cz int32 `json:"cz"` Items []PropRestockItem4229938 `json:"items"` } // // Reads Configs/PropRestock.json — the general-purpose restock table for blob props the client // never instantiates on its own (indoor furniture, vending machines, claw machines, mini-game // cabinets, basketball courts, bouncy beds, market stalls...). // // // Same data source and same channel as the gym equipment, just not scoped to a venue: // the placements come straight out of GadgetBlob.tmpPackedGadgetExtendInfo.basePackedInfo // via GetGadgetBlobData(uid), so pathId / pos / euler / uniqueId are all official values. // ⛔ Two hard rules that this whole feature rests on (both learned 2026-09-23): // 1. PackedGadgetInfo.iScale must be 0. 100 yields an entity that exists, has the // right name/uid/position and even a working interact point, but transform scale (0,0,1) — // a zero-scale shell with no prefab and a collapsed interact radius. // 2. Pushing blob data into the client does not instantiate anything. Driving // ReadHeadInfo + InitSimpleGrid + OnAoiAddBlobCfgByGridIndex leaves // GadgetInstanceRoot's child count untouched; only SyncGadgetAdd / AddList / AOI do. // Doors (SpoonSlot_*) are deliberately excluded from the generated file: the T2-11 door // system already owns them and a double push fights itself. // var ( PropRestockCatalog4229938_cacheLock sync.RWMutex PropRestockCatalog4229938_cached = false PropRestockCatalog4229938_entries []any ) // LoadPropRestockCatalog4229938 loads catalog records from client config directory func LoadPropRestockCatalog4229938(configDir string) ([]any, error) { PropRestockCatalog4229938_cacheLock.RLock() if PropRestockCatalog4229938_cached { defer PropRestockCatalog4229938_cacheLock.RUnlock() return PropRestockCatalog4229938_entries, nil } PropRestockCatalog4229938_cacheLock.RUnlock() PropRestockCatalog4229938_cacheLock.Lock() defer PropRestockCatalog4229938_cacheLock.Unlock() if PropRestockCatalog4229938_cached { return PropRestockCatalog4229938_entries, nil } filePath := filepath.Join(configDir, "PropRestockCatalog.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) } } PropRestockCatalog4229938_entries = result PropRestockCatalog4229938_cached = true return result, nil } // // Every cell of within of the player's // cell (Chebyshev distance). // // // ⛔ The raid filter is not optional: (cx,cz) is only unique inside one map. Cell (28,39) in // 新启 and cell (28,39) in 重霄·凌云 are different places with different props, and stocking // one for the other litters the wrong map with furniture. // func PropRestockCatalog4229938_NearCell(raid uint32, cx int32, cz int32, radiusCells int32) []PropRestockCell4229938 { // Catalog helper PropRestockCatalog4229938_NearCell return nil } // How many cells the table holds for one map (0 = nothing captured there yet). func PropRestockCatalog4229938_CountForRaid(raid uint32) int32 { // Catalog helper PropRestockCatalog4229938_CountForRaid return 0 } func PropRestockCatalog4229938_LoadTriple() any { // Catalog helper PropRestockCatalog4229938_LoadTriple var zero any; return zero } // // u64 that the capture script emits as a JSON string (no double round-trip). System.Text.Json // throws InvalidOperationException ("requires an element of type 'Number'") if you call // TryGetUInt64 on a string element, so accept both shapes — one bad uid must not kill the file. // // // ★★★★★ 2026-09-28 —— uid 超出 ulong 时**必须仍给一个稳定的值**,绝不能退化成 0。 // 病因(实测,用户报「两个摊叠在一起」):PropRestock.json 里 Gadget_DaQiQiu_Position // 那条的 uid 是 "19900000000000007919",而 ulong.MaxValue = 18446744073709551615 // ⇒ 旧代码 ulong.TryParse 失败 ⇒ 返回 0 ⇒ SpawnGadgetAsync 里 // id = uniqueId != 0 ? uniqueId : state.NextSpawnedGadgetId++ **每次补货都 mint 一个新 id** // ⇒ 同一坐标 (-4750.5,168.6,-2827.6) yaw 315.0 上出现**第二个**气球板实例。 // 实测两次补货:08:41:55 → id=9500000000000;08:42:00 → id=9500000000008。 // (同一批的另外 26 件 uid 正常 ⇒ id 完全一致 ⇒ 幂等;只有这一件在涨。) // 为什么"幂等"是关键:StartGadgetGrid / StopGadgetGrid 会 ResetPropRestock, // 而客户端**按 gadgetUId 去重**——id 稳定 ⇒ 重摆同一件是 no-op;id 每次变 ⇒ 每重摆一次就多一个。 // R95 只删掉了摊位表那一份重复(保留本表这份),**没发现本表这份的 uid 根本解析不出来**。 // 修法:按 mod 2^64 回绕(唯一、确定、跨次补货恒定),并把这件事记进 LoadWarnings。 // func PropRestockCatalog4229938_U64(source any, name string) uint64 { // Catalog helper PropRestockCatalog4229938_U64 return 0 } func PropRestockCatalog4229938_Str(source any, name string) string { // Catalog helper PropRestockCatalog4229938_Str return "" } func PropRestockCatalog4229938_Floats(source any, name string, take int32) []float32 { // Catalog helper PropRestockCatalog4229938_Floats return nil }