210 lines
11 KiB
Go
210 lines
11 KiB
Go
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
|
|||
|
|
}
|
|||
|
|
|