package routers
import (
"context"
"sync"
)
var _ = sync.Mutex{}
var _ = context.Background
//
// Teleport: inbound client/GM requests plus server-push primitives used by the admin panel.
// Lua (RPCSerializeAuto WriteTeleportOption/WritePreTeleportOption) is the wire truth;
// dump.cs TeleportOption/PreTeleportOption field order is mirrored 1:1 in TeleportMethods.
//
// 按「离玩家最近的那家店」选店(没有位置信息时兜底西门)。
func GameRouter_Teleport_NearestShop(ctx context.Context, playerPos any) any {
// Router implementation for GameRouter_Teleport_NearestShop
var zero any; return zero
}
// 入口与出口共用 config=8,靠 beforeResName 里的 in/out 区分方向。
func GameRouter_Teleport_IsExitCutscene(ctx context.Context, option any) bool {
// Router implementation for GameRouter_Teleport_IsExitCutscene
return true
}
//
// config=8 且 position 是全零时,照 donor 的做法用真实目标点填空:
// 进场填 Interior(工位)、离场填 Leave(街边),并同步 afterPosition / afterRot。
//
func GameRouter_Teleport_PrepareCarShopOption(ctx context.Context, conn any, option any, state any) any {
// Router implementation for GameRouter_Teleport_PrepareCarShopOption
var zero any; return zero
}
//
// ★★ 景点 / 电梯传送门(config == 2,MapentranceConfig.Type == 1)——
// 2026-09-30 三修:「进电梯不黑屏、但落地还在原地」的真正元凶在这里。
//
前两修都改错了地方
// 一修:开始推 SyncTeleport ⇒ 黑屏解了(ReportPostTeleportFinish result=0)。
// 二修:SyncTeleport.MapEntranceId 从起点门改成落点门(343)⇒ 依旧原地
// (13:38:43 实机:下发门=343 落点=(2277.54,160.60,1412.50),玩家仍在
// [INDOOR] indoorId=332 绿屿公馆大厅)。
// 真因(捐赠端在改装店上用实机日志证明过,见本文件顶部注释 ①)
//
// config=8 的 option.position / afterPosition 客户端填的是 (0,0,0),
// 过场结束时客户端会「应用 afterPosition」 ⇒ 不改写就把人和车甩到世界原点
// (donor 2026-09-22 18:28 实测,SyncChangeIndoor(4) 进店成功 2 秒后掉回家)。
//
// 电梯这条链路一模一样:RpcUtils.lua:88 只填了
// beforePosition = cfg.Coordinate、afterResName = JiguanEndTimeline,
// afterPosition 是全零、customAfterTrans = false。
// 而 SyncPreTeleportOption 我们一直是原样回显客户端那份 ⇒ 过场一结束,
// 客户端就把全零(或退化后的当前点)当成落点应用 —— 跟我们推的 SyncTeleport 无关。
// 修法(照改装店 PrepareCarShopOption 的成型做法)
// 在 echo 之前原地改写 option:position / afterPosition 都填推导出来的
// 落点,customAfterTrans = true。目的地仍由
// JiguanElevatorCatalog4229938 从 MapentranceConfig 推。
// 起点门用 beforePosition 反查(客户端就是拿 cfg.Coordinate 填的,
// 不依赖 extParams 的解析 —— 那份契约我们还没有)。
// SyncTeleport 那一帧保留(它是解开 loading 的必要条件),但不再是落点来源。
// ★★ 起点门从哪来:extParams 里的 JSON(2026-09-30 实机取证)
// 客户端 RpcUtils.lua:90-105 会把 extParams.mapEntranceId 一起塞进
// PreTeleportOption.extParams(一个 JSON 字符串)。13:38:37 实机报文:
//
// {"configId":2, ..., "customAfterTrans":false, "afterPosition":{"X":0,"Y":0,"Z":0},
// "beforePosition":{"X":2257.781,"Y":0,"Z":1458.242}, "beforeRot":{"X":0,"Y":90,"Z":0},
// "extParams":"{\"mapEntranceId\":337}"}
//
// ⛑ 光靠 beforePosition 是分不开的:337 与 340 的 Coordinate 完全相同
// (大厅同一个门位上下两个按钮),只有 EntranceFacing 差 180°(90 / -90)。
// 所以优先读 extParams;读不到才退回「位置 + beforeRot.Y 朝向」匹配。
// 回滚 = gameplay.mapEntrance.jiguanElevator=false。
//
// true = 这是电梯事务且已改写,调用方去 echo。
func GameRouter_Teleport_PrepareJiguanOption(ctx context.Context, conn any, option any) bool {
// Router implementation for GameRouter_Teleport_PrepareJiguanOption
return true
}
//
// 从 PreTeleportOption.extParams 里取 mapEntranceId。
// 客户端塞的是一个 JSON 字符串(实测 {"mapEntranceId":337};不是电梯事务时是 {})。
// 解析失败一律返回 0,由调用方退回坐标匹配。
//
func GameRouter_Teleport_TryReadMapEntranceIdFromExtParams(ctx context.Context, extParams any) uint32 {
// Router implementation for GameRouter_Teleport_TryReadMapEntranceIdFromExtParams
return 0
}
//
// 按坐标 + 朝向反查景点 / 电梯门(客户端把 cfg.Coordinate 填进 beforePosition、
// 把 cfg.EntranceFacing 填进 beforeRot.Y)。
// ⛑ 必须先按「位置 + 朝向」筛:绿屿大厅 337 与 340 的 Coordinate 完全相同
// (同一个门位的两个按钮),只有 EntranceFacing 差 180°(90 / -90),
// 而它俩的目的地一个是高楼层、一个是车库。只看坐标会挑到列表里靠后的那个。
//
func GameRouter_Teleport_FindJiguanDoorByPosition(ctx context.Context, p any, facing float32) any {
// Router implementation for GameRouter_Teleport_FindJiguanDoorByPosition
var zero any; return zero
}
//
// ★★ 车库出库事务(config == 7 且 beforeResName 含 out)的落点改写。
// 2026-09-30 —— 与景点电梯三修、改装店 PrepareCarShopOption 是同一个坑:
// 过场结束时客户端应用的是 afterPosition,而客户端自己那份算不出来。
// 为什么客户端算不出来
// GarageManager.lua PlayLoadingCutscene 里(反编译后比较符方向已按语义还原):
//
// afterWaypointName = houseCfg.GarageOutWaypoint -- Spoon 航点 id
// afterTLName = houseCfg.ExitTimeline[2]
// if afterWaypointName and afterTLName and afterTLName ~= "" then
// local afterPos = gSpoonMgr:GetWayPointPositionByNameOrId(afterWaypointName)
// if afterPos then ... preTeleportOption.afterPosition = afterPos ... end
// end
//
// 两条都断:① 服务端没有 Spoon 位置表,客户端在私服里也查不到航点;
// ② 素光小筑(11200006)的 ExitTimeline 只有 1 个元素
// (["loading_garage_out03"],别的房都是 ["out01","out03"])⇒
// afterTLName = ExitTimeline[2] = nil ⇒ 整块跳过 ⇒
// afterPosition 留全零(实测 23:38:24 报文
// afterPosition:(0,0,0) customAfterTrans:false)⇒ 过场一结束就被甩到世界原点 / 黑屏。
// 修法
// 原地改写 option,然后由调用方自己 echo 一份
// SyncPreTeleportOption(不走那个受 gameplay.mapEntrance 开关约束的
// 通用 echo 分支 —— 车库不该被景点配置牵连):position / afterPosition 都填
// ResolveGarageExitLanding 推出来的落点、customAfterTrans = true。
// 过场起点(beforePosition)客户端同样拿不到 Spoon 航点(也是全零),
// 用玩家最近上报的位置兜底 —— 出库时玩家正坐在车位上的车里,那就是车位格。
// ⚠️ PreTeleportOption 没有 MapEntranceId 字段(那是
// SyncTeleport 的 TeleportOption 才有),落点门 id 只在
// OnAskTeleportFromHouseGarage 推 SyncTeleport 那一步用。
// 回滚 = gameplay.houseGarage.enabled=false(本方法直接返回,option 逐字节不变)。
//
// true = 这是车库出库事务且已改写,调用方去 echo。
func GameRouter_Teleport_PrepareGarageExitOption(ctx context.Context, conn any, option any) bool {
// Router implementation for GameRouter_Teleport_PrepareGarageExitOption
return true
}
// 把人 + 车一起挪到 dst/yaw(改装店进场/离场共用)。
func GameRouter_Teleport_PushPlayerAndVehicleAsync(ctx context.Context, conn any, state any, dst any, yaw float32, reason string) any {
// Router implementation for GameRouter_Teleport_PushPlayerAndVehicleAsync
var zero any; return zero
}
//
// 「这个 AskTeleport 事务要播 timeline」的判据:任一 timeline 资源名非空。
// 决定是否把客户端发来的 option 原样回显(见 MapEntranceSettings.EchoPreTeleport)。
// 实测数据点:地铁 config=5 → after=loading_metro_end_1;改装店
// config=8 → loading_garage_in02(后者走它自己的分支,不经过这里)。
//
func GameRouter_Teleport_HasTimeline(ctx context.Context, option any) bool {
// Router implementation for GameRouter_Teleport_HasTimeline
return true
}
// 玩家最近开过的那辆车(服务端 Proxy 生成/F1 召唤才有)。
func GameRouter_Teleport_LastDrivenVehicleFix(ctx context.Context, session *GameSession) any {
// Router implementation for GameRouter_Teleport_LastDrivenVehicleFix
var zero any; return zero
}
func GameRouter_Teleport_ResolveShopVehicleId(ctx context.Context, session *GameSession) uint64 {
// Router implementation for GameRouter_Teleport_ResolveShopVehicleId
return 0
}
func GameRouter_Teleport_GetStateIfExists(ctx context.Context, session *GameSession) any {
// Router implementation for GameRouter_Teleport_GetStateIfExists
var zero any; return zero
}
// Client -> server: native loading-flow request. Accept it, remember the correlation
// ids, and let the client drive its own flow; admin pushes use the outbound path below.
func GameRouter_Teleport_AskTeleport(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_AskTeleport
var zero any; return zero
}
func GameRouter_Teleport_AskTeleportToParkingWaypoint(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_AskTeleportToParkingWaypoint
var zero any; return zero
}
func GameRouter_Teleport_AskTeleportToLeavingWaypoint(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_AskTeleportToLeavingWaypoint
var zero any; return zero
}
func GameRouter_Teleport_ReportPreTeleportFinish(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_ReportPreTeleportFinish
var zero any; return zero
}
func GameRouter_Teleport_ReportPostTeleportFinish(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_ReportPostTeleportFinish
var zero any; return zero
}
//
// 延迟补发一帧落点(jiguanHardSet 的第二发)。失败只记日志,不影响主流程。
// 走 TcpSession.NotifyAsync(uint, byte[], CancellationToken) +
// CancellationToken.None:本方法的调用者(handler)已经返回,请求上下文可能已失效。
//
func GameRouter_Teleport_ReassertJiguanLandingAsync(ctx context.Context, session *GameSession, unitId uint64, landing any) any {
// Router implementation for GameRouter_Teleport_ReassertJiguanLandingAsync
var zero any; return zero
}
//
// 延迟补发一帧车库出库落点(ReportPostTeleportFinish 硬设之后 500ms 的第二发)。
// 与景点电梯四修同一套路:loading 面板还在淡出时第一帧会被吞掉。
// 同坐标重复下发无副作用;失败只记日志,不影响主流程。
// 走 TcpSession.NotifyAsync(uint, byte[], CancellationToken) +
// CancellationToken.None:调用它的 handler 已经返回,请求上下文可能已失效。
//
func GameRouter_Teleport_ReassertGarageLandingAsync(ctx context.Context, session *GameSession, unitId uint64, landing any) any {
// Router implementation for GameRouter_Teleport_ReassertGarageLandingAsync
var zero any; return zero
}
//
// 当前该用哪个单位 id 发 SyncUnitPositionAndFacing。
// 与 TeleportPlayerInstantAsync 取同一个来源,保证补发那一帧打在同一实体上。
//
func GameRouter_Teleport_ResolveActiveUnitId(ctx context.Context, session *GameSession) uint64 {
// Router implementation for GameRouter_Teleport_ResolveActiveUnitId
return 0
}
// GM service Teleport(x, z): UXRPCMethodArgs62733679. Log it and treat it as a
// same-map XZ jump hint (Y stays at the last reported height).
func GameRouter_Teleport_GmTeleportXZ(ctx context.Context, conn any, msg any) any {
// Router implementation for GameRouter_Teleport_GmTeleportXZ
var zero any; return zero
}
//
// Instant move without a loading screen: SyncUnitPositionAndFacing (68030312) with the default
// SetPositionType.SwitchSpirit (3). Updates the tracked transform optimistically.
// ⚠️ Not SyncUnitPosition_P (68233969): that one is refused for the local avatar —
// NetworkGhostModule.ForceSetPositionAndFacing needs NetworkRoleType.Proxy, and the main
// character is not a proxy (Player.log: "Role(None)不可强设位置!!").
//
func GameRouter_Teleport_TeleportPlayerInstantAsync(ctx context.Context, rpcCtx *RpcContext, x float32, y float32, z float32, facing any, reason string) any {
// Router implementation for GameRouter_Teleport_TeleportPlayerInstantAsync
var zero any; return zero
}
//
// Loading-flow move: SyncPreTeleportOption (64167855) + SyncTeleport (64030755)
// with a fresh teleport id. The client is expected to answer with
// ReportPreTeleportFinish / ReportPostTeleportFinish (logged by handlers above).
//
func GameRouter_Teleport_TeleportPlayerWithLoadingAsync(ctx context.Context, rpcCtx *RpcContext, x float32, y float32, z float32, facing float32, reason string) uint64 {
// Router implementation for GameRouter_Teleport_TeleportPlayerWithLoadingAsync
return 0
}
func GameRouter_Teleport_GetWorldStateStatic(ctx context.Context, rpcCtx *RpcContext) any {
// Router implementation for GameRouter_Teleport_GetWorldStateStatic
var zero any; return zero
}
// 一趟电梯事务的落点(起终点门 id 只用于日志/排查)。
type PendingJiguanLanding struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Facing float32 `json:"facing"`
SourceEntranceId uint32 `json:"sourceEntranceId"`
DestEntranceId uint32 `json:"destEntranceId"`
}
// ----------------------------------------------------------------------------------
// 改装店(ApplyCar / RepairShop)—— 2026-09-25 从俄语上游 donor 的 repair-shop.zip 移植。
//
// 入场链路(客户端 ApplyCarManager.lua:152 EnterRepairShop):
// 触发区(AskVehicleEnterArea) → CheckCanEnterCarShop → AskTeleport(config=8)
// → 服务端 echo SyncPreTeleportOption → 客户端播 beforeResName 时间轴
// → ReportPreTeleportFinish → **AskTeleportToParkingWaypoint**
// → 服务端推 SyncTeleport(人) + SyncTeleportVehicle(车) → ReportPostTeleportFinish。
//
// ⛑⛑ 两处 donor 用实机日志证明、不看代码绝对猜不出来的坑:
// ① config=8 的 option.position / afterPosition 客户端填的是 (0,0,0),
// 过场结束时客户端会「应用 afterPosition」⇒ 不改写就把人和车甩到世界原点
// (donor 2026-09-22 18:28 实测, SyncChangeIndoor(4) 进店成功 2 秒后掉回家)。
// ② 关键一步是分布 AskTeleportToParkingWaypoint / LeavingWaypoint 里推 *人的*
// SyncTeleport:只 ack 不推 ⇒ 客户端永远停在黑屏(17:11 / 17:31 实测),
// post-finish 永远不来。而且 pre-finish 里不能推(会把驾驶中的玩家拽到半空)。
// ----------------------------------------------------------------------------------
// 一间 Smokey Drift 的四个落成点(坐标见 ShopInteriors)。
type ShopAnchor struct {
Entrance TeleportVec3 `json:"entrance"`
Interior TeleportVec3 `json:"interior"`
Facing float32 `json:"facing"`
EnterPos TeleportVec3 `json:"enterPos"`
EnterYaw float32 `json:"enterYaw"`
Leave TeleportVec3 `json:"leave"`
LeaveYaw float32 `json:"leaveYaw"`
}