package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // // 一次景点 / 电梯传送(AskEnterRaidByMapEntrance)的目的地。 // Resolved = false 表示推导失败,X/Y/Z // 退回成入口自己那一格(也就是「原地不动」),Note 写明原因。 // 退回而不是不推 SyncTeleport,是因为「不推」正是黑屏的根因(见下)。 // EntranceId = 落点那一层的那扇门的 MapentranceConfig.Id。 // ⛑⛑ 2026-09-30 二修:它就是「进电梯不黑屏、但落地还在原地」的根因所在 —— // SyncTeleport 必须把目的地的门 id 填进 MapEntranceId, // 不能填玩家按下的那扇起点门。见 TryResolve 上的长注释。 // type JiguanDestination4229938 struct { X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` Facing float32 `json:"facing"` EntranceId uint32 `json:"entranceId"` RaidId uint32 `json:"raidId"` Resolved bool `json:"resolved"` Note string `json:"note"` } // // 景点 / 电梯传送门(MapentranceConfig.Type == 1)的目的地推导。 // 2026-09-30 新增,修「进电梯黑屏、ESC 返回不了」。 //

症状与根因

// 客户端 RpcUtils.AskEnterRaidByMapEntrance(E:\lua\lua\LX6\Utils\RpcUtils.lua:88): // // preTeleportOption.beforeResName = cfg.JiguanStartTimeline -- "loading_elevator02" // preTeleportOption.beforePosition = cfg.Coordinate -- 电梯门自己的位置 // preTeleportOption.loadingResName = cfg.JiguanLoadingTimeline -- 337 是 "" // preTeleportOption.afterResName = cfg.JiguanEndTimeline -- "loading_general_end" // extParams.mapEntranceId = mapEntranceId // gLoadingManager:AskTeleport(LoadingConfig.EnterExitRoomId=2, option, extParams, cb) // -- cb 里(= ReportPreTeleportFinish 被 ack 之后): // gClientToGameDelegate:AskEnterRaidByMapEntrance(mapEntranceId) // // 注意 afterPosition 是全零、customAfterTrans=false —— 客户端不知道目的地, // 它等的是服务端那一帧 SyncTeleport。而 2026-09-30 01:07:55 的实测日志里, // 服务端 handler 只 ReturnEmptyOkAsync ⇒ 客户端永远停在 loading(黑屏,ESC 无效), // 86 秒后 ReportPostTeleportFinish result=2 放弃。 // 对照 01:07:39 成功那次(地铁 / 大地图传送):客户端发 AskPlayerChangePositionByScenePortal, // 服务端推 SyncTeleport ⇒ result=0。 //

目的地从哪来

// 客户端只给 JiguanTeleportPos(Spoon 航点 id,如 1917305)与 // JiguanTeleportPosName(如 TeleportPos_337_338_339_target)。 // 本地没有 Spoon 图(SceneSpoonNames 只有 Chaos_bulbfly* 那几个), // 所以拿不到航点坐标。但 MapentranceConfig 自己就把答案写全了: // 电梯是「一个竖井、每层两个门」的结构,门的坐标就是那一层的坐标, // 门的目标就是它去的那一层。以绿屿公馆(HouseConfig 11200003)为例: // // 337/338/339 (2257.78, 0, 1448~1458) → 1917305 ┐ 一层(大厅) // 340/341/342 (2257.78, 0, 1448~1458) → 1917531 ┘ 同坐标、目标不同 // 343 (2277.54, 160.6, 1412.50) → 1917575 高楼层(= VisitorEntrance 附近) // 344 (2277.54, 160.6, 1412.50) → 1917531 // 345 (2257.81, -18.9, 1458.17) → 1917575 地下车库 // 346 (2257.81, -18.9, 1458.17) → 1917305 // // 于是:目标 T 所在的那一层 = 本组所有楼层里,唯一「没有门去往 T」的那一层。 // (337 的目标 1917305 被「大厅 + 车库」的门指向 ⇒ 剩下高楼层就是它。) // 分组的连接规则三条(任一条成立即同组): // ① 目标 id 相同(337 与 346 都去 1917305); // ② 坐标几乎相同(337/338/339 与 340/341/342 同在大厅); // ③ 三维距离 < gameplay.mapEntrance.jiguanGroupRadius(默认 60m,跨「大厅↔高楼层 168m」不够, // 但靠 ① / ② 已经连通;60m 是为了兜住 317/318 这类只有两个门的短竖井)。 //

已验证

// 2026-09-30 用 Python 把 93 行配置全跑了一遍(同一套规则): // 45 个 Type==1 门里 41 个带 JiguanTeleportPos,全部推导出唯一/可质心化的目的地; // 4 个只有 Spoon 名字没 id 的(326/327 AlanA、319/320/321 同一目标多门)落到退回分支。 // 337 → (2277.54, 160.60, 1412.50),与 HouseConfig 11200003.VisitorEntrance // (2281.36, 160.73, 1411.68) 相距约 4m(同一个高楼层平台)。 //

★★ 2026-09-30 二修:坐标对了,人还是没动 —— 缺的是「目的地门 id」

// 一修之后 packets 里看得到正确落点,客户端也回了 // ReportPostTeleportFinish result=0(黑屏解了),但玩家站在原地。 // 对照同一次会话里成功的那一帧(01:28:47 大地图传送): // // 成功:SyncTeleport Position=(2287.65,0.01,1471.65) MapEntranceId=215 ← 215 就是目的地入口 // 失败:SyncTeleport Position=(2277.54,160.60,1412.50) MapEntranceId=337 ← 337 是玩家按的那扇门 // // 差别只有 MapEntranceId。客户端侧 LX6.GUI.CommonFuncTeleportFlowInfo // (dump TypeDefIndex 9525)持有 preTeleportOption / teleportOption 两份, // 并且有 public uint AfterTargetMapEntranceId 与 // InitAfterData() / ReParse_AfterData() —— ReParse_AfterData 不吃参数, // 只能读 teleportOption(也就是服务端推下来的那一帧)去重算落地数据。 // 而本条链路里客户端自己的 afterPosition 是全零、customAfterTrans=false // ⇒ 它只能靠 teleportOption 反推 ⇒ 拿到 337 的坐标 = 大厅 = 原地。 // (成功那条链路客户端自己填了 afterPosition,两个来源一致,所以从来没暴露过这个坑。) // 修法:SyncTeleport.MapEntranceId 填落点那一层的门 id,Position 也换成 // 那扇门自己的坐标(不再用质心)—— 两个来源逐字节一致,走哪条分支都对。 // 绿屿公馆对照:337/338/339 → 343(高楼层);340/341/342 → 345(车库);346 → 343;344 → 345。 // 观澜北岸对照:347/348/354 → 351(高楼层);349/350/352 → 353(车库);351/353 → 347(大厅)。 //

回滚

// gameplay.mapEntrance.jiguanElevator=false ⇒ 逐字节回到「只 ack」的旧行为。 // gameplay.mapEntrance.jiguanEntranceIdMode="source" ⇒ 回到一修(只推坐标、仍填起点 id)。 // const ( // jiguanGroupRadius 缺省 / 非法时的兜底值(米)。 JiguanElevatorCatalog4229938_DefaultGroupRadius float32 = 60 // 「这两条门在同一层」的坐标容差(米)。 JiguanElevatorCatalog4229938_SameFloorEpsilon float32 = 1.0 ) var ( JiguanElevatorCatalog4229938_cacheLock sync.RWMutex JiguanElevatorCatalog4229938_cached = false JiguanElevatorCatalog4229938_entries []any ) // LoadJiguanElevatorCatalog4229938 loads catalog records from client config directory func LoadJiguanElevatorCatalog4229938(configDir string) ([]any, error) { JiguanElevatorCatalog4229938_cacheLock.RLock() if JiguanElevatorCatalog4229938_cached { defer JiguanElevatorCatalog4229938_cacheLock.RUnlock() return JiguanElevatorCatalog4229938_entries, nil } JiguanElevatorCatalog4229938_cacheLock.RUnlock() JiguanElevatorCatalog4229938_cacheLock.Lock() defer JiguanElevatorCatalog4229938_cacheLock.Unlock() if JiguanElevatorCatalog4229938_cached { return JiguanElevatorCatalog4229938_entries, nil } filePath := filepath.Join(configDir, "JiguanElevatorCatalog.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) } } JiguanElevatorCatalog4229938_entries = result JiguanElevatorCatalog4229938_cached = true return result, nil } // // 推导 这台电梯的落点。 // 返回 false 只代表「入口 id 不在表里」;其余所有失败路径都会 // 返回 true + JiguanDestination4229938.Resolved=false // 的「原地不动」兜底,好让调用方永远能推出一帧 SyncTeleport 解开客户端的 loading。 // = gameplay.mapEntrance.jiguanLandingOverrides(可空)。 // func JiguanElevatorCatalog4229938_TryResolve(sourceEntranceId uint32, groupRadius float32, destination JiguanDestination4229938, note string) bool { // Catalog helper JiguanElevatorCatalog4229938_TryResolve return false } // func JiguanElevatorCatalog4229938_TryResolve_2(sourceEntranceId uint32, groupRadius float32, overrides []JiguanLandingOverride, destination JiguanDestination4229938, note string) bool { // Catalog helper JiguanElevatorCatalog4229938_TryResolve_2 return false } // // 纯几何推导(不含落点覆盖)。见 TryResolve(uint, float, out JiguanDestination4229938, out string) // 的完整说明。 // func JiguanElevatorCatalog4229938_TryResolveCore(sourceEntranceId uint32, groupRadius float32, destination JiguanDestination4229938, note string) bool { // Catalog helper JiguanElevatorCatalog4229938_TryResolveCore return false } // // 把「门标记坐标」换成「人真正站得住的那一格」。 // 优先级:(配置,热重载)> // MeasuredLandings(内置实测表)> 几何推导出来的门坐标。 // 键都是 落点门 id(SyncTeleport.MapEntranceId 填的那个); // 「原地不动」兜底时它等于起点门 id,所以也能按起点门 id 覆盖。 // func JiguanElevatorCatalog4229938_ApplyLanding(dest JiguanDestination4229938, overrides []JiguanLandingOverride) JiguanDestination4229938 { // Catalog helper JiguanElevatorCatalog4229938_ApplyLanding var zero JiguanDestination4229938; return zero } // 两条门是否属于同一台电梯(同目标 / 同坐标 / 距离小于分组半径)。 func JiguanElevatorCatalog4229938_Linked(a MapEntranceEntry4229938, b MapEntranceEntry4229938, radius float32) bool { // Catalog helper JiguanElevatorCatalog4229938_Linked return false } func JiguanElevatorCatalog4229938_Distance(a any, b any) float32 { // Catalog helper JiguanElevatorCatalog4229938_Distance return 0 } func JiguanElevatorCatalog4229938_Find(parent []int32, i int32) int32 { // Catalog helper JiguanElevatorCatalog4229938_Find return 0 } func JiguanElevatorCatalog4229938_Union(parent []int32, a int32, b int32) { // Catalog helper JiguanElevatorCatalog4229938_Union }