198 lines
11 KiB
Go
198 lines
11 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// ★★ 生活日程的正式通道 —— SyncFavorNpcSpoonAgentId(64003213)。
|
||
// 背景(09-25 实机):里希按日程站在跑步机旁边 / 上面,但只会站着玩手机。
|
||
// 在出场包 AgentSyncClientInfo.AgentDataSetsActivityCfgId 里填活动 id 不够
|
||
// —— 那个字段只是「这个 NPC 归哪条活动管」,客户端还需要知道
|
||
// 「他此刻占着几号 spoon 位、站哪个点、朝哪边」才会去播那条活动的表演。
|
||
// 取证来源(补丁和工具\DUMPS\readable_world_dump_4229938_final.zip):
|
||
// <list type="bullet">
|
||
// <item>06_RUNTIME_CODE/.../Auto.Client/UX/Game/IGameToClient.cs:
|
||
// void SyncFavorNpcSpoonAgentId(uint agentTag, int spoonAgentId, UXVector3 Position,
|
||
// FavorNpcBusyInfo busyInfo = null);</item>
|
||
// <item>.../LX6/Units/Module/LifeScheduleNpc/LifeScheduleNpcModule.cs:
|
||
// AttachNet(net, activityId, storySpoonAgentId, hasAmbientLoopPose, ambientLoopPos,
|
||
// ambientLoopFacing) + SetAmbientLoopPoseContext /
|
||
// TryGetStoryLoopPose / BeginAmbientEnterAnchoredPlay
|
||
// ⇒ 表演的「入场动画」是从 loop pose 反推锚点后播的。</item>
|
||
// <item>.../L50/StoryClient/Story/World/S009/SLifeScheduleNpcNet.cs(Spoon 的
|
||
// 「日程表NpcNet(Client S树)」):复制的字段就是
|
||
// NpcId / NpcName / ActivityId / AgentTag / SpoonAgentId / HasAmbientLoopPose /
|
||
// AmbientLoopPos / AmbientLoopFacing ⇒ 逐项对上了上面那条 RPC。</item>
|
||
// <item>.../Auto.Client/UX/Game/NpcTimeTableInfo.cs:
|
||
// CurrentSpoonAgentId + SpoonPosition + 五段
|
||
// NpcScheduleInfo{ActivityId, StartDaySecond, Position, SpoonAgentId, EndDaySecond,
|
||
// RaidId} ⇒ 登录帧里那三处我们一直填 0(见 RuntimePayloadFactory)。</item>
|
||
// </list>
|
||
// ⛑⛑ spoonAgentId 到底该填什么,官方没有配置表(它住在客户端 Spoon 资产里,
|
||
// 扫遍 01_CONFIGS / 02_INTERIORS / 03_ZONEGRAPHS / 04_WORLD_JSON 都是零命中)。
|
||
// 目前最自圆其说的取法是「填这个角色自己的 AgentId」—— 因为出场包
|
||
// AgentSyncClientInfo.SpoonAgentId(GameRouter.DonorNpc.cs:234)官方/donor
|
||
// 填的就是 NpcFormworkId,两边同一个值客户端才可能对上号。所以默认值
|
||
// agentId,同时留 fixed 手动试号。
|
||
// 线格式(RPCDeserializeAuto.lua:1601 的 reader):
|
||
// <code>
|
||
// u32 agentTag | i32 spoonAgentId | f32 x, f32 y, f32 z | [0x00 = busyInfo 为 null]
|
||
// </code>
|
||
// ⛑ 基本类型不带存在标记(Plate 那条已实机证过:裸写就能解出来)。
|
||
//
|
||
//
|
||
// 推一条 SyncFavorNpcSpoonAgentId(64003213)。
|
||
//
|
||
// 好感 NPC 的 AgentTag(里希 = 12)。
|
||
// spoon 位号。
|
||
func GameRouter_NpcLife_PushNpcSpoonAgentAsync(ctx context.Context, session *GameSession, agentTag uint32, spoonAgentId int32, x float32, y float32, z float32, cfg any) int32 {
|
||
// Router implementation for GameRouter_NpcLife_PushNpcSpoonAgentAsync
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 常驻 NPC 落地后的挂钩:给它补一条「你现在占着几号 spoon 位」。
|
||
// ⛑ 只在它真的有活动(有坐标、有活动 id)且拿到了 AgentTag 时才发,
|
||
// 否则等于告诉客户端「去一个不存在的位子」。
|
||
//
|
||
func GameRouter_NpcLife_EnsureNpcSpoonAsync(ctx context.Context, session *GameSession, spiritId uint32, agentId uint32, activityId uint32, x float32, y float32, z float32) any {
|
||
// Router implementation for GameRouter_NpcLife_EnsureNpcSpoonAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 落地后的 POI 兜底:gameplay.npcLife.poiActionId 非 0 且开关打开时,
|
||
// 给刚放出来的单位补一帧 SyncAgentPlayPOIAction。
|
||
//
|
||
func GameRouter_NpcLife_EnsureNpcPoiActionAsync(ctx context.Context, session *GameSession, unitId uint64) any {
|
||
// Router implementation for GameRouter_NpcLife_EnsureNpcPoiActionAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// ★★ 「这个角色此刻该演什么」的唯一裁决点(09-25 23:0x 加,把里希那一套推广到
|
||
// 所有有日程的角色)。常驻摆放(GameRouter.RosterNpc)与切人留场
|
||
// (GameRouter.Switching)都走这里 —— 两处各写一份必然漂移。
|
||
// 优先级:活动级 rosterNpc.activityPoiOverrides (按
|
||
// AgentDataSetsActivityConfig.Id) > 角色级 rosterNpc.poiOverrides
|
||
// (按 spiritId) > 调用方给的默认值。
|
||
// ⛑⛑ 为什么「配了 POI 就不带活动 id」:生活日程的 ambient 会盖掉 POI
|
||
// —— 里希最早就是「一边打电话一边跑步」(规则 163)。
|
||
// 没配映射的活动照旧带活动 id,走生活日程(那是官方那条路,只是没表演)。
|
||
// ⛑ 朝向返回 float?:调用方要能区分「没配」(沿用各自原来的朝向)
|
||
// 和「配了 0」。常驻原来恒 0,切人留场原来用玩家切走时的朝向。
|
||
//
|
||
func GameRouter_NpcLife_ResolveActivityPresentation(ctx context.Context, spiritId uint32, activityId uint32, defaultPoiActionId uint32) any {
|
||
// Router implementation for GameRouter_NpcLife_ResolveActivityPresentation
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// ★★ 落地坐标的**活动级微调**(gameplay.rosterNpc.activityPoiOverrides[].offsetX/Y/Z)。
|
||
// 官方 MapPosition 是策划打的点,跟场景家具常常差几十厘米;这种偏差没有表可查,
|
||
// 只能实机挪。字段语义(世界轴 / 朝向 0 时右 = +X)见
|
||
// ActivityPoiOverride.OffsetX。
|
||
// ⛑ 三个调用点必须**都**走这里(常驻摆放 / 切人留场 / 主动切到日程点),
|
||
// 否则同一个角色会因为出场方式不同而站在两个不同的地方。
|
||
// ⛑ 没配 / 查不到 ⇒ 原样返回,零开销、零行为变化。
|
||
//
|
||
func GameRouter_NpcLife_ApplyActivityOffset(ctx context.Context, activityId uint32, x float32, y float32, z float32) any {
|
||
// Router implementation for GameRouter_NpcLife_ApplyActivityOffset
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 按 NpcLifeSettings.SpoonAgentIdMode 决定 spoon 位号。
|
||
func GameRouter_NpcLife_ResolveSpoonAgentId(ctx context.Context, cfg any, agentId uint32) int32 {
|
||
// Router implementation for GameRouter_NpcLife_ResolveSpoonAgentId
|
||
return 0
|
||
}
|
||
|
||
// =================================================================================
|
||
// ① 时刻表 SyncFavorNpcTimeTableInfos(64219939) / SyncAgentTagTimeTableInfo(64400736)
|
||
// =================================================================================
|
||
//
|
||
// ★★ spoon 帧的硬前提:客户端 NpcDaliyManager.lua:143
|
||
// OnSyncFavorNpcSpoonAgentId 一进门就判
|
||
// table.isNilOrEmpty(self.NpcTimeTableInfos[agentTag]),空则 return
|
||
// ⇒ 没有时刻表时 64003213 整帧被静默丢弃(不报错、不打日志)。
|
||
// 09-25 19:36 第一次实机推 spoon「零反应」就是这个原因。
|
||
// 线格式(RPCDeserializeAuto.lua:2327 / :3003):
|
||
// <code>
|
||
// 64219939: FF + Int7(count) + 每项 [u32 agentTag] + [Complex NpcTimeTableInfo]
|
||
// 64400736: u32 agentTag + [Complex NpcTimeTableInfo]
|
||
// </code>
|
||
// ⛑ NpcTimeTableInfo 本体交给 UxSerializer —— 它和登录帧
|
||
// FavorNpcDailyScheduleInfos 用的是同一套(Complex,TypeMark 0xFF,
|
||
// Position/SpoonPosition 是 Struct 不带标记,TempSchedule 可空),
|
||
// 别手搓字节。只有最外层字典要自己写:UxSerializer.Serialize(Dictionary)
|
||
// 顶层没有 field ⇒ count 会按 Int32 写,而客户端这里读的是 ReadDict7Bit。
|
||
//
|
||
func GameRouter_NpcLife_PushNpcTimeTableAsync(ctx context.Context, session *GameSession, focusAgentTag uint32, spoonAgentId int32, cfg any) int32 {
|
||
// Router implementation for GameRouter_NpcLife_PushNpcTimeTableAsync
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 造一份 Dictionary<uint, NpcTimeTableInfo>。与登录帧同源(NpcDailySchedule.json
|
||
// 的五段 × NpcActivityCatalog.json 的 MapPosition),区别是
|
||
// 那一条会把 CurrentSpoonAgentId /
|
||
// SpoonPosition(以及当前时段的 SpoonAgentId)填上真值 —— 登录帧那三处恒为 0。
|
||
//
|
||
func GameRouter_NpcLife_BuildTimeTables(ctx context.Context, focusAgentTag uint32, spoonAgentId int32, cfg any) map[string]any {
|
||
// Router implementation for GameRouter_NpcLife_BuildTimeTables
|
||
return nil
|
||
}
|
||
|
||
// 当前时段落在五段里的第几段(找不到就 -1)。与 NpcActivityCatalog.CurrentFor 同判据。
|
||
func GameRouter_NpcLife_CurrentSliceIndex(ctx context.Context, agentTag uint32, daySecond int32) int32 {
|
||
// Router implementation for GameRouter_NpcLife_CurrentSliceIndex
|
||
return 0
|
||
}
|
||
|
||
// spoon 坐标 = 当前时段那条活动的 MapPosition(查不到就是零向量)。
|
||
func GameRouter_NpcLife_SpoonPositionFor(ctx context.Context, agentTag uint32) any {
|
||
// Router implementation for GameRouter_NpcLife_SpoonPositionFor
|
||
var zero any; return zero
|
||
}
|
||
|
||
// =================================================================================
|
||
// ① 兜底 POI 动作 SyncAgentPlayPOIAction(68459398)
|
||
// =================================================================================
|
||
//
|
||
// 让某个单位直接播一个 POI 动作(UrbanDiversityPOIActionConfig)。
|
||
// 通道是已验证过的原生帧(GameRouter.Ambient.cs:892 同款):
|
||
// 跑步机 = 198「站姿_运动_跑步机跑步」(MainActionGroup 400 / MainActions [86190])。
|
||
// ⛑ 开了生活日程(AgentDataSetsActivityCfgId 非 0)时客户端会盖掉它
|
||
// ⇒ 要验这条就把 gameplay.rosterNpc.dailyLife 关掉。
|
||
//
|
||
func GameRouter_NpcLife_PushNpcPoiActionAsync(ctx context.Context, session *GameSession, unitId uint64, poiActionId uint32) int32 {
|
||
// Router implementation for GameRouter_NpcLife_PushNpcPoiActionAsync
|
||
return 0
|
||
}
|
||
|
||
// =================================================================================
|
||
// ② C2S 查询:客户端主动问「好感 NPC 都在哪儿」
|
||
// =================================================================================
|
||
//
|
||
// C2S AskQueryAllFavorNpcAgentPos(63357395) / AskQueryFavorNpcAgentPos(63216726)。
|
||
// ★★ 为什么这条比上面那条还重要:**客户端自己在问**,而 `unknown-methods.log` 里
|
||
// 63357395 已经出现过 54 次 —— 我们每次都回「空字典」(`DefaultReturnCatalog4229938`
|
||
// 的 `dict7-empty` = `FF 01`)⇒ 客户端眼里「城里一个好感 NPC 都不在场」,
|
||
// 那 `LifeScheduleNpcModule` 自然不会给任何人排表演。
|
||
// 返回类型(客户端 C# ClientToGameDelegate):
|
||
// UXRPCTask<Dictionary<uint, FavorNpcPosInfo>>,
|
||
// FavorNpcPosInfo { uint RaidId; UXVector3 Position; }(key = AgentTag)。
|
||
// 线格式:`FF` + Int7(count+1) + 每项 [u32 agentTag] + [FF + u32 RaidId + f32×3]。
|
||
// ⛑ count 用 UxWriter.Int7(内部已 +1),别自己再加。
|
||
//
|
||
func GameRouter_NpcLife_OnAskQueryFavorNpcAgentPos(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_NpcLife_OnAskQueryFavorNpcAgentPos
|
||
var zero any; return zero
|
||
}
|
||
|