package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 房产(House)所有权 —— 2026-09-30。用户报的症状: // 「手机 app 里买房产会播动画 + 显示已购买,但实际走到那栋房子显示未购买」。 // 根因(一句话):客户端判断「这套房是不是我的」只有一条路径 —— // gBuyHouseUtils.CheckHasBuyTheHouse(houseId) → // gPlayerManager.infoMinor.bindData.housesInfo.HouseInfoList → // PlayerInfoMinorData.lua:54 的 t.housesInfo = info.InfoMinor.housesInfo。 // 也就是说所有权完全由登录包决定,而服务端从来没往里写过。 // 手机里那套「已购买」是 BuyHouseUtils.BuyTheHouse 拿到 error=0 之后的 // 本地乐观行为(自己 SendMessage(ON_BUY_HOUSE_SUCCESS) + 开 // HOUSE_REWARD_PANEL 播 HouseConfig.GameVideo), // 服务端当时既不记账也不推包 —— 所以面板一关 / 一重登 / 一开大地图,客户端重算一次就翻回「未购买」。 // 本文件负责三件事: // // 记账:GrantHouseOwnershipAsync —— 写 // WorldEntryState.Progress.OwnedHouseIds(PlayerProgressStore 落盘), // 并推一次 SyncAddHouse(64459483) 让本次会话立刻生效 // (登录包里没有它,客户端得靠这条增量补上)。 // 推送:PushSyncAddHouseAsync —— 线格式见 // HouseOwnershipWire4229938。 // 读房:AskLoadHouseData(63109556) —— 客户端 C# 侧 // AskLoadHouseData(uint houseId) : UXRPCTask<HouseBuildingClientData> // 唯一的用途是拿「这栋房子的装修内容」。2026-09-30 起这里还负责下发 // 「房子自带的家具」(gameplay.houseFurniture,见 // HouseFurnitureTemplateCatalog4229938)—— 在此之前一律回 57 字节空房, // 那就是玩家看到的毛坯房。同时保证 OwnerPid 是自己而不是 0 // (兜底表给的是 0)。 // // 本轮明确不做(用户拍板「只做所有权落地」+「自带家具」): // 车库 / 停车(SyncHouseParking)、家具编辑 / 购买 / 摆放 // (AskAddBuildHouseIndoor / AskSaveHouseConfiguration / AskModifyHouse / // 墙体编辑 / livehouse)、鱼缸与收藏品的运行时数据(FurnitureGadgetDataDict)、 // 访客与点赞、以及真实扣款(保持免费)。 // 配置依赖:Configs/HouseConfig.json(5 条,见 // HouseCatalog4229938)。商店侧 NpcShopCatalog.json 早已含 94100080 // 「新启房产」,5 件商品齐全,所以 AskNpcShopCommodityInfo 不用改。 // // // 购买落地:记所有权 + 落盘 + 推 SyncAddHouse。 // ⛔ 只在「第一次拿到」时推。BuyHouseUtils.SyncAddHouse 是 // table.insert(不是 set),同一套房推两次客户端列表里就有两条重复项。 // 已拥有时静默返回 —— 登录包已经把这条给过客户端了。 // 写进日志的结果串(granted / already-owned / no-world)。 // func GameRouter_House_GrantHouseOwnershipAsync(ctx context.Context, session *GameSession, houseId uint32, log any) string { // Router implementation for GameRouter_House_GrantHouseOwnershipAsync return "" } // // 推 SyncAddHouse(64459483):payload 是**单个** HouseBaseInfo 的 complex。 // ⛔ 直接把 HouseActiveInfo 交给 NotifyAsync<T> —— // UxSerializer 对非 Inline 的 [UxContract] 类型会自己写那一个 0xFF 包装, // 正好对上客户端的 Base.ReadComplex(reader, Auto.Reader[368])。 // **不要**再包一层 Inline 壳(多一个 0xFF ⇒ 整包错位)。 // func GameRouter_House_PushSyncAddHouseAsync(ctx context.Context, session *GameSession, houseId uint32, log any) any { // Router implementation for GameRouter_House_PushSyncAddHouseAsync var zero any; return zero } // // 63109556 — 客户端 C# 侧唯一调用点: // UXRPCTask<HouseBuildingClientData> AskLoadHouseData(uint houseId)。 // 入参就是一个裸 u32(ClientToGameDelegate.lua 的 // AskLoadHouseData_Serializer 只写一个 primitive),返回一个 complex(590)。 // 2026-09-30 起这条路要下发「房子自带的家具」。在此之前我们一律回 // 权威兜底表那份 57 字节全空 HouseBuildingClientData // (DefaultReturnCatalog4229938 的 case 63109556u,逐字节由 // Auto.Reader[590] 推出)—— 只把 OwnerPid 换成自己的 pid, // 因为兜底给的是 0(「无主」),跟所有权落地直接冲突。 // 空 FloorBuildInfoDict 就是毛坯房的直接原因。 // 现在:houseId → HouseConfig.BuildId → HouseBuildConfig.IndoorId / // Floors / FurnitureTemplateIds → HouseFurnitureTemplateConfig, // 把模板树翻成 PlacedFurnitureInfo 树塞进 FloorBuildInfoDict。 // 详见 HouseFurnitureTree4229938 与 // HouseBuildingClientData4229938(线格式逐字对照在那儿)。 // 兜底仍然保留:关掉开关 / houseId 查不到 / 房子没有可装修体 // (岸北悠居公寓 BuildId = 0)/ 模板树为空 —— 任一条命中就回 57 字节空房, // 和改动前逐字节一致。 // func GameRouter_House_AskLoadHouseData(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_House_AskLoadHouseData var zero any; return zero } // // 「自带家具」下发分支。true = 已经回过包,调用方直接 return。 // 任何一步查不到都返回 false,让调用方走 57 字节兜底 —— 这样出问题时的表现 // 和改动前一模一样(毛坯),不会退化成半截包 / 空包。 // func GameRouter_House_TryReturnHouseFurnitureAsync(ctx context.Context, conn any, msg any, houseId uint32, owned bool) bool { // Router implementation for GameRouter_House_TryReturnHouseFurnitureAsync return true } // // FloorBuildInfoDict 的键。 // 结论(2026-09-30 取证):键 = 楼层号,1 基,地面层 = 1,取值 1..Floors。 // 所以默认 floor 就是正解。 // 证据链(全部来自客户端自己的文件,不是推测): // // HouseBuildConfig.FloorsName = [{floor:1,__RAW__name:"1F"}]; // 素光小筑 [{1,"1F"},{2,"2F"}],且长度恰好等于 Floors。 // HouseBuildConfig.BuildBound / CameraBound / CameraTopViewPoint // 一律用 floor: 1,2(同时带 IndoorBoundId: 1,2,数值与 floor 相同)。 // HomeBuildPanelStore_Floor.lua:SelectOption(initFloor - 1)、 // houseCamera.floor = index + 1 ⇒ 游戏侧 floor 是 1 基、只有 UI 选项是 0 基; // GetFloorByPlayerPosition 两处兜底都 return 1。 // LuaGmUtils.lua:166 的 // for floorKey, indoorBuildInfo in pairs(buildData.FloorBuildInfoDict) —— 变量名就叫 // floorKey;而且它对每个键都加载一遍家具(去重表 loadedFurnitureIds 建在 // 键循环体内部)⇒ 多写键 = 家具被创建多遍。所以默认绝不能「把所有候选键都写上碰运气」。 // HouseUtils.lua 三个函数(Add/Remove/ModifyOneFurnitureInfo)里的 // floor = floor or 0 只是「参数缺失」的兜底;0 本身是非法值(会走 // "无法找到楼层[0]的家具信息" 的 warn 分支)。 // // indoor / all 保留为诊断开关:GmSyncHouseIndoorBuildInfo(houseId, // indoorId, …) 那条 GM 链路确实按 indoorId 组织,万一登录链路将来被证伪可立刻切过去验证。 // all 会同时写 1..Floors 与 IndoorId,一定产生重复家具,只用于确认 // 「客户端到底取哪个键」。 // func GameRouter_House_ResolveFurnitureFloorKeys(ctx context.Context, mode any, build any) []any { // Router implementation for GameRouter_House_ResolveFurnitureFloorKeys return nil } func GameRouter_House_LogFurnitureTree(ctx context.Context, log any, houseId uint32, nodes []any, depth int32) any { // Router implementation for GameRouter_House_LogFurnitureTree var zero any; return zero }