305 lines
15 KiB
Go
305 lines
15 KiB
Go
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 三修:「进电梯不黑屏、但落地还在原地」的真正元凶在这里。
|
||
// <h3>前两修都改错了地方</h3>
|
||
// 一修:开始推 SyncTeleport ⇒ 黑屏解了(ReportPostTeleportFinish result=0)。
|
||
// 二修:SyncTeleport.MapEntranceId 从起点门改成落点门(343)⇒ 依旧原地
|
||
// (13:38:43 实机:下发门=343 落点=(2277.54,160.60,1412.50),玩家仍在
|
||
// [INDOOR] indoorId=332 绿屿公馆大厅)。
|
||
// <h3>真因(捐赠端在改装店上用实机日志证明过,见本文件顶部注释 ①)</h3>
|
||
// <blockquote>
|
||
// config=8 的 option.position / afterPosition 客户端填的是 (0,0,0),
|
||
// 过场结束时客户端会「应用 afterPosition」 ⇒ 不改写就把人和车甩到世界原点
|
||
// (donor 2026-09-22 18:28 实测,SyncChangeIndoor(4) 进店成功 2 秒后掉回家)。
|
||
// </blockquote>
|
||
// 电梯这条链路一模一样:RpcUtils.lua:88 只填了
|
||
// beforePosition = cfg.Coordinate、afterResName = JiguanEndTimeline,
|
||
// afterPosition 是全零、customAfterTrans = false。
|
||
// 而 SyncPreTeleportOption 我们一直是原样回显客户端那份 ⇒ 过场一结束,
|
||
// 客户端就把全零(或退化后的当前点)当成落点应用 —— 跟我们推的 SyncTeleport 无关。
|
||
// <h3>修法(照改装店 PrepareCarShopOption 的成型做法)</h3>
|
||
// 在 echo 之前原地改写 option:position / afterPosition 都填推导出来的
|
||
// 落点,customAfterTrans = true。目的地仍由
|
||
// JiguanElevatorCatalog4229938 从 MapentranceConfig 推。
|
||
// 起点门用 beforePosition 反查(客户端就是拿 cfg.Coordinate 填的,
|
||
// 不依赖 extParams 的解析 —— 那份契约我们还没有)。
|
||
// SyncTeleport 那一帧保留(它是解开 loading 的必要条件),但不再是落点来源。
|
||
// <h3>★★ 起点门从哪来:extParams 里的 JSON(2026-09-30 实机取证)</h3>
|
||
// 客户端 RpcUtils.lua:90-105 会把 extParams.mapEntranceId 一起塞进
|
||
// PreTeleportOption.extParams(一个 JSON 字符串)。13:38:37 实机报文:
|
||
// <code>
|
||
// {"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}"}
|
||
// </code>
|
||
// ⛑ 光靠 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,而客户端自己那份算不出来。
|
||
// <h3>为什么客户端算不出来</h3>
|
||
// GarageManager.lua PlayLoadingCutscene 里(反编译后比较符方向已按语义还原):
|
||
// <code>
|
||
// 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
|
||
// </code>
|
||
// 两条都断:① 服务端没有 Spoon 位置表,客户端在私服里也查不到航点;
|
||
// ② 素光小筑(11200006)的 ExitTimeline 只有 1 个元素
|
||
// (["loading_garage_out03"],别的房都是 ["out01","out03"])⇒
|
||
// afterTLName = ExitTimeline[2] = nil ⇒ 整块跳过 ⇒
|
||
// afterPosition 留全零(实测 23:38:24 报文
|
||
// afterPosition:(0,0,0) customAfterTrans:false)⇒ 过场一结束就被甩到世界原点 / 黑屏。
|
||
// <h3>修法</h3>
|
||
// 原地改写 option,然后由调用方自己 echo 一份
|
||
// SyncPreTeleportOption(不走那个受 gameplay.mapEntrance 开关约束的
|
||
// 通用 echo 分支 —— 车库不该被景点配置牵连):position / afterPosition 都填
|
||
// ResolveGarageExitLanding 推出来的落点、customAfterTrans = true。
|
||
// 过场起点(beforePosition)客户端同样拿不到 Spoon 航点(也是全零),
|
||
// 用玩家最近上报的位置兜底 —— 出库时玩家正坐在车位上的车里,那就是车位格。
|
||
// <para>⚠️ PreTeleportOption 没有 MapEntranceId 字段(那是
|
||
// SyncTeleport 的 TeleportOption 才有),落点门 id 只在
|
||
// OnAskTeleportFromHouseGarage 推 SyncTeleport 那一步用。</para>
|
||
// 回滚 = 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"`
|
||
}
|
||
|