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

211 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}