package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // Party-camp furniture the server places itself — 露营的烤炉 / 帐篷 / 篝火 / 野餐垫 / 灯 / 野餐车. // WHY (2026-09-26, round 44). 用户站在露营点 2.8 m 处实测「家具全无」:烤炉、帐篷一件都没有, // 而我们**确实**把营地那一格(32,59)通过 grid-AOI 下发过了。原因和健身房器材一模一样: // 这批 `Gadgets_Party_*` 只活在客户端的关卡 blob(LX6.Item.GadgetBlob)里,而 // SyncGadgetGridAOIDecrease(68956797) 这条下发**不驱动** // GadgetBlob.OnAoiAddBlobCfgByGridIndex ⇒ 烘焙里的道具一件都不会自己出现。 // 用户那句「是不是和健身器材一样」是对的。见 TickGymEquipmentAsync。 // ★ 判据(回答「是不是开派对才刷」):**不是**。家具烘焙在 raid 23300888 = 主城 free-roam, // 而露营/舞会的 PartyConfig.RaidId 也是 23300888 ⇒ 派对就地举行、不切副本 ⇒ 场地家具只能是 // 预先常驻的世界装点。旁证:同一批 prefab 还被当作天台夜市(-378.28,130.50,-497.87)的 // 常驻装点摆着;营地 400 m 圈内全是改车店/公厕/时装店/象棋摊等主城常驻机关。 // ⇒ 所以这里是**常驻摆放**(玩家走进场地半径就摆),而不是「开派对时才摆」。 // ⛔ PackedGadgetInfo.iScale 必须是 0(100 ⇒ transform scale (0,0,1) 零尺寸空壳)。 // Rollback: world.content.partyCamp.enabled = false. // // // ★★★ 2026-09-30:**就地开局** —— 把指定 partyId 的整片场地搬到玩家脚下。 // // // 官方把舞会 13030003 摆在 y ≈ 80 的悬空层(那层没有地板,实机够不到), // 所以开派对时由 GameRouter 的 StartSingleParty 分支调本方法, // 把家具整片平移到玩家坐标;下一次 grid tick 就会按新坐标摆出来。 // ⛑⛑ 必须同时把场地从 Placed 里摘掉: // Placed 是「每会话每场地只摆一次」的去重 HashSet,而一次误判 // (玩家临时站在矩形外 ⇒ INDOOR-GATE 全挡)就会把场地永久毒死 // (2026-09-30 踩过,见 2026-09-30.md §45.2)。 // 同理,同一会话重开派对时也必须解掉,否则第二场一件家具都没有。 // 传入的 必须是地面基准(玩家上报 y − 眼高 ≈ 1.07 m), // 不是玩家上报值 —— 角色上报的是胶囊中心,直接用会整体浮空 1 m。 // 是开派对那一刻玩家的朝向(度)。partyVenuePropFacePlayer // 打开时,家具整片会按它旋转,让最远那件(舞会 = 灯光控制台)落在玩家正前方; // 传 0(或开关关闭)则保持官方的世界方位。 // func GameRouter_PartyCamp_SetPartyVenueAtPlayer(ctx context.Context, session *GameSession, partyId uint32, x float32, y float32, z float32, facingDeg float32) any { // Router implementation for GameRouter_PartyCamp_SetPartyVenueAtPlayer var zero any; return zero } func GameRouter_PartyCamp_PartyCampStateOf(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_PartyCamp_PartyCampStateOf var zero any; return zero } // Reconnect / scene switch: forget what was stocked so the next visit re-places it. func GameRouter_PartyCamp_ResetPartyCamp(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_PartyCamp_ResetPartyCamp var zero any; return zero } // // Called from the grid-AOI tick (which already knows the player position). Places every camp // the player has walked into, once per camp per session. // func GameRouter_PartyCamp_TickPartyCampAsync(ctx context.Context, session *GameSession, state any, px float32, pz float32) any { // Router implementation for GameRouter_PartyCamp_TickPartyCampAsync var zero any; return zero } type PartyCampSessionState struct { SyncRoot any `json:"syncRoot"` // Camp sites already stocked this session. One shot per site, like the gym. Placed []string `json:"placed"` PlacedCount int32 `json:"placedCount"` WarnedEmpty bool `json:"warnedEmpty"` // ★ 2026-09-30 就地开局:把整片场地临时搬到玩家脚下(0 = 未启用,走配表锚点)。 VenueAtPlayerPartyId uint32 `json:"venueAtPlayerPartyId"` VenueAtPlayerX float32 `json:"venueAtPlayerX"` VenueAtPlayerY float32 `json:"venueAtPlayerY"` VenueAtPlayerZ float32 `json:"venueAtPlayerZ"` // // 就地开局那一刻玩家的朝向(度)。家具整片靠它旋转到玩家正前方 // (见 SetPartyVenueAtPlayer)。0 = 保持官方世界方位。 // VenueAtPlayerFacingDeg float32 `json:"venueAtPlayerFacingDeg"` }