This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/internal/gameserver/routers/game_router_npc_life.go
T

197 lines
11 KiB
Go
Raw Normal View History

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) &gt; 角色级 rosterNpc.poiOverrides
// (按 spiritId) &gt; 调用方给的默认值。
// ⛑⛑ 为什么「配了 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&lt;uint, NpcTimeTableInfo&gt;。与登录帧同源(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&lt;Dictionary&lt;uint, FavorNpcPosInfo&gt;&gt;,
// 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
}