package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // One citizen to place: formwork (AgentConfig id) + the POI action it plays. type CivilianSpawnItem struct { NpcFormworkId uint32 `json:"npcFormworkId"` PoiActionId uint32 `json:"poiActionId"` } // // 让一个走市民通道刷出来的 NPC 穿上「某个可玩角色身上那套搭配」(切人留场用)。 // 官方原生写法就在 NPC 出场包里,没有第二处: // AgentSyncClientInfo.AIAgentInfo(LinkAIAgentInfo)→ // Fashion(LinkAIFashionInfo = SuitId + WearInfo)→ // WearInfo(OtherPlayerSpiritWearFashionsInfo,带单件 WearFashionInfoList)。 // 这就是「把别人的角色放进大世界」那条路 —— 麻将/保龄球的 AI 对手读的也是 // unit.ClientData.AIAgentInfo(MajiangGame_UI.lua:181 / BowlingCharacter.lua:414)。 // ⛑ 市民通道顶层的 FashionSuitId 只能填套装 id,而玩家存档 // config/player-progress.json 里 9/9 个角色 FunctionSuitId 全为 0 // (纯散件搭配,4~10 件)⇒ 走套装那条路对「自定义搭配」根本没用,必须走这里。 // ⛑ wire 顺序(Drmk.Proxy/proxy/lua/RPCSerializeAuto.lua:4706): // Uid(u64) → FightSpiritId(u32) → Fashion(complex,nilable) → Nickname(string,nilable) // → NameId(u32) → AvatarImageId(u32) → VehicleId(u32); // Fashion = SuitId(u32) → WearInfo(complex,不可空)。与 // DonorNpcLinkAIAgentInfo4229938 字段顺序一一对应,别改顺序。 // type CivilianLinkDress struct { Uid uint64 `json:"uid"` FightSpiritId uint32 `json:"fightSpiritId"` SuitId uint32 `json:"suitId"` FashionIds []uint32 `json:"fashionIds"` HiddenParts byte `json:"hiddenParts"` Nickname *string `json:"nickname"` } // // Fourth entity channel: citizens owned by the client's Aether crowd simulation. // The server's whole job here is to announce these entities once and then remember that it did: // walking, obstacle avoidance and hit reactions are entirely client-side (that is why the donor // build gets full pedestrian behaviour without simulating anything). Nothing in this file ticks, // moves or fights an entity — and we deliberately do NOT re-send anything periodically, because // replaying the POI would cancel the client's own reactions. // Aether init (SyncAetherAIInitDatas) IS re-sent before every batch — the donor does that too, // and dropping it once cost us a full day of "panel says ok, world stays empty". // func GameRouter_Civilians_CivilianGroup(ctx context.Context, id int32) any { // Router implementation for GameRouter_Civilians_CivilianGroup var zero any; return zero } // // T0-28:**这个 AgentConfig 模板能不能当路人放出去**。三道独立筛子全过才算市民, // 行人池(PedFormworkPool)和地块市民池(BuildItemPool)共用它, // 这样两条线不会再出现"街上刷的是市民、地块刷的是店员"的不一致。 // ⛑️ 为什么是三道而不是一道(`CivilianGroups.json` 本身不够用): // 那张表是按"给每个 POI 动作配一个**体型匹配的人形**"建的 —— 只保证是人形 + 阵营 2, // 不保证是路人,也不保证模型的脸不一样。所以要在它之上再筛。 // 三道:① id 段白名单(4013)② **GeneralModelId 黑名单**(通用临时模型,最关键的一道) // ③ 名字关键词黑名单(店员 / 刷怪 / 剧情职能)。 // func GameRouter_Civilians_IsCivilianFormwork(ctx context.Context, id uint32) bool { // Router implementation for GameRouter_Civilians_IsCivilianFormwork return true } // // Place citizens in front of the player on a jittered grid: the slots are shuffled so the // group does not read as a parade formation, and each slot is offset by at most a quarter of // the spacing so two citizens can never land on the same spot. // Returns every generated entity id. // func GameRouter_Civilians_SpawnCiviliansNearPlayerAsync(ctx context.Context, session *GameSession, items []any, spacing float32) any { // Router implementation for GameRouter_Civilians_SpawnCiviliansNearPlayerAsync var zero any; return zero } // ---- Seeding citizens onto the real land plots (T0-8) ------------------------------------ // Same four-step announce sequence as SpawnCiviliansNearPlayerAsync; the only difference is // where the positions come from. Instead of a grid in front of the player we ask // CivilianDistrictCatalog4229938 for the actual plots around them, then let each plot's own // 24 h pedestrian curve decide how many of its pavement spots get someone on them. // // The catalog is data produced by _gen_civilian_districts.py from the client's own urban // diversity settings + pedestrian lane network, so a citizen ends up on pavement with a real // ground height rather than floating at the player's Y. // // Fill every plot around the player with citizens, how many decided by each plot's pedestrian // flow at <hour>. Hour -1 means "use the world clock the client is currently showing". // func GameRouter_Civilians_SpawnCiviliansByDistrictAsync(ctx context.Context, session *GameSession, radius float32, maxTotal int32, hour int32) any { // Router implementation for GameRouter_Civilians_SpawnCiviliansByDistrictAsync var zero any; return zero } // // 在**官方过街等待区**的槽位上铺市民(2026-10-02 新增,配置 gameplay.civilianWaitArea)。 // ★ 点位来源 = paths.pedSpawns(zonegraph_ped_spawns.csv,4070 槽), // 真身是客户端 MassAI.MassCrowd.CrowdWaitAreaData:行人沿 ConnectedLanes 汇入、 // 占一个槽位站定(Radius + bOccupied 防重叠)、等相位、走 CrossLaneHandle 过街。 // ⛑ 那套是客户端原生 ECS,服务端驱动不了;我们只是把自建 NPC 放到同一批坐标上, // 让画面上像「这撮人也在路口等灯」。 // ★ 与 SpawnCiviliansByDistrictAsync 的分工:那边按「生态区 + 时段人流」铺出一整条街的人, // 这边只在路口四角补一小撮;两者叠加才像真城市。两边都有关开关,互不干扰。 // func GameRouter_Civilians_SpawnCiviliansAtWaitAreasAsync(ctx context.Context, session *GameSession, radius float32, maxTotal int32, hour int32) any { // Router implementation for GameRouter_Civilians_SpawnCiviliansAtWaitAreasAsync var zero any; return zero } // // 路口等待区的 24 小时人流权重(0~1)。深夜几乎没人,早晚通勤双峰最高。 // ★ 与地块那边的 HourlyFlow 是两套数据:等待区 CSV 不带人流维度,只能按常识给一条曲线。 // func GameRouter_Civilians_WaitAreaFlowAt(ctx context.Context, hour int32) float32 { // Router implementation for GameRouter_Civilians_WaitAreaFlowAt return 0 } // // 用水平最近的**地块站位**的 Y 当高度采样(等待区 CSV 的 y 恒为 0,必须补)。 // ★ 两级查找:先按区中心 + 半径粗筛(681 个区),再在命中区里逐点比。 // 全表 13786 点逐点扫也能出结果,但每个等待区点都扫一遍会到千万次量级。 // ⛑ 找不到就退回 (通常是玩家当前 Y),不硬填 0。 // func GameRouter_Civilians_GroundYAt(ctx context.Context, x float32, z float32, fallbackY float32) float32 { // Router implementation for GameRouter_Civilians_GroundYAt return 0 } func GameRouter_Civilians_NormaliseYaw(ctx context.Context, degrees float32) float32 { // Router implementation for GameRouter_Civilians_NormaliseYaw return 0 } func GameRouter_Civilians_DistrictItemPool(ctx context.Context, categories []any) []any { // Router implementation for GameRouter_Civilians_DistrictItemPool return nil } func GameRouter_Civilians_BuildItemPool(ctx context.Context, categories []any) []any { // Router implementation for GameRouter_Civilians_BuildItemPool return nil } // // 这条 POI 动作在当前筛选配置下能不能用。查不到(表里没有这个 id)一律放行 —— // 宁可放过也不要因为表缺失把整个池清空。 // func GameRouter_Civilians_CivilianPoiAllowed4229938(ctx context.Context, poiActionId uint32, cfg any) bool { // Router implementation for GameRouter_Civilians_CivilianPoiAllowed4229938 return true } func GameRouter_Civilians_Shuffle(ctx context.Context, source []any) []any { // Router implementation for GameRouter_Civilians_Shuffle return nil } // // Server-driven walk for the citizens spawned this session. // This exists to settle one specific question: whether the client plays a gait for movement the // server pushes, or only interpolates between points. SyncUnitPositionAndFacing carries a moveId // (UX.Game.BattleMoveActionType) and every call site in this build used to send 0 = Unknown, // which is exactly what "sliding" looks like from the outside. Pushing the same trajectory with // moveId=0 and moveId=2 (WalkFront) and watching both is the only way to tell. // Citizens are the test vehicle because their ids are ours and their channel is verified. This // does not take movement away from the client's Aether crowd — it only pushes positions on top, // and nothing here runs unless the operator asks for it. // func GameRouter_Civilians_WalkCiviliansAsync(ctx context.Context, session *GameSession, moveId byte, distance float32, durationSeconds float32) any { // Router implementation for GameRouter_Civilians_WalkCiviliansAsync var zero any; return zero } // // The four-step announce sequence for one citizen. Copied field-by-field from the donor build // that was verified in-game; do not "simplify" it — dropping SyncManagedLogicAgent leaves the // unit without a simulation owner and the full-model state machine gets disabled right after // the ECS→GO handoff (which is exactly what T-pose looks like from the outside). // func GameRouter_Civilians_SendCivilianAsync(ctx context.Context, session *GameSession, item any, entityId uint64, x float32, y float32, z float32, facing float32, dress any, activityCfgId uint32, skipPhone bool, skipHackPhone bool, aiSettingOverride uint32, treeNameOverride any) any { // Router implementation for GameRouter_Civilians_SendCivilianAsync var zero any; return zero } // // 切人留场专用:走 SyncRaidBattleUnitAgent(68402349) 通道把老角色刷成 NPC。 // ⛑⛑ 为什么不用 SendCivilianAsync(Aether 静态 NPC 通道):那条线没有地方放单件衣服 —— // 顶层只有 FashionSuitId(套装 id),而 AIAgentInfo.Fashion.WearInfo // 客户端根本不读(09-25 两次实测:包发出去了、6 件衣服都在,但模型依旧默认服装)。 // RaidBattleUnitAgent 同时带 SuitId + FashionIdList(单件列表) // + 完整 46 字段 AgentSyncClientInfo,是唯一能指定单件的原生通道。 // 序列照抄 GameAdminBridge.SpawnAgentAsync(dbnb-0.2 实机验证过的那条): // SyncLogicAgentEnter → SyncManagedLogicAgent → SyncRaidBattleUnitAgent → // SyncUnitOccur → SyncAgentCampInfo(camp=0) → SyncAttachBattleModule → POI。 // ⛑ 友方三件套(规则 132):不发 SyncForceEnemyRelation、camp 填 0、 // 也不注册进服务端敌人账本(这里压根不走那条路)。 // func GameRouter_Civilians_SendLeftBehindAgentAsync(ctx context.Context, session *GameSession, agentId uint32, unitId uint64, x float32, y float32, z float32, facing float32, poiActionId uint32, dress any, forceGo bool, activityCfgId uint32) any { // Router implementation for GameRouter_Civilians_SendLeftBehindAgentAsync var zero any; return zero }