package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // Fifth entity channel: functional NPCs at fixed city spots (shop clerks, ticket sellers, ...). // Same four-step announce sequence as the citizen channels — this file does not invent a spawn // path, it only supplies different coordinates. Instead of "a plot's pavement spots, how many // decided by the hour" the positions are固定的: one per venue, already snapped onto pavement by // the generator, so a shop clerk ends up where the shop actually is. // What the NPC *does* is not our problem here: F-key behaviour (open a shop panel, buy a ticket, // start a mahjong match) is driven entirely by the client's own lua through // AgentConfig.InteractSetting. The server just has to put a body there. That is deliberate — it // is the whole reason this card is cheap. // Two ways in, sharing one placement helper so they can never disagree: // • the follow loop (EnsureFunctionalNpcs) — how they normally appear: walk up to // a shop and its clerk is standing there; // • the panel button (SpawnFunctionalNpcsAsync) — "put them all down now". // func GameRouter_FunctionalNpc_FunctionalNpcStateOf(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_FunctionalNpc_FunctionalNpcStateOf var zero any; return zero } // World-ready hook: start the follow loop. Idempotent; no-op when disabled. func GameRouter_FunctionalNpc_EnsureFunctionalNpcs(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_FunctionalNpc_EnsureFunctionalNpcs var zero any; return zero } // Disconnect / scene switch: stop the loop. The client clears the entities itself. func GameRouter_FunctionalNpc_StopFunctionalNpcs(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_FunctionalNpc_StopFunctionalNpcs var zero any; return zero } // How many the follow loop currently has standing. Panel display only. func GameRouter_FunctionalNpc_FunctionalNpcActiveCount(ctx context.Context, session *GameSession) int32 { // Router implementation for GameRouter_FunctionalNpc_FunctionalNpcActiveCount return 0 } func GameRouter_FunctionalNpc_RunFunctionalNpcLoopAsync(ctx context.Context, session *GameSession, owner any, token context.Context) any { // Router implementation for GameRouter_FunctionalNpc_RunFunctionalNpcLoopAsync var zero any; return zero } func GameRouter_FunctionalNpc_TickFunctionalNpcsAsync(ctx context.Context, session *GameSession, owner any, cfg any) any { // Router implementation for GameRouter_FunctionalNpc_TickFunctionalNpcsAsync var zero any; return zero } func GameRouter_FunctionalNpc_RemoveFunctionalNpcAsync(ctx context.Context, session *GameSession, state any, owner any, id uint64, spot any) any { // Router implementation for GameRouter_FunctionalNpc_RemoveFunctionalNpcAsync var zero any; return zero } // // ★★★ R93(2026-09-27)—— **落脚高度吸附**:把点位声明的 Y 换成「该 XZ 上真实地面/物件的高度」。 // 为什么必须做:FunctionalNpcSpots.json 的 Y 是用 CivilianDistricts 人行道点 // 反向找的(见该文件 note)。那套人行道点只在**主城平地**有数据,一旦点位的 XZ 落在 // **高台/庙会/室内**,找出来的就是别处(常常是 y≈0/2 那一层)的 Y。 // 实测标本(2026-09-27):Tea Shop Owner(40954014)/Tea Stall Owner(40954022) 声明 Y=2.00, // 而同 XZ 上 2528 件场景物一律 **y≈166~172** ⇒ NPC 被生成在**地下 167 m**, // 现象 = 用户所说的「摊位的npc呢」(看着像根本没刷,其实刷了、埋了)。 // 数据源 = config/scene_item_placements.json(mtime 热重载,4877 件带真实 y)。 // 取点位 XZ **半径 内最近的若干件**的 y 中位数当落脚高度: // 中位数对「同一点位旁边既有台面又有地面」的情况比最近邻稳。 // 命中不到(荒地/新点位)⇒ **原样返回**,绝不猜。 // func GameRouter_FunctionalNpc_SnapGroundY(ctx context.Context, x float32, y float32, z float32, samples int32, snapped float32, radius float32) float32 { // Router implementation for GameRouter_FunctionalNpc_SnapGroundY return 0 } // // One placement: announce + remember. Returns the entity id, or 0 when the spot's ids are not // in the catalogs (a bad row in the json must not kill the batch). // func GameRouter_FunctionalNpc_PlaceFunctionalNpcAsync(ctx context.Context, session *GameSession, state any, owner any, spot any) uint64 { // Router implementation for GameRouter_FunctionalNpc_PlaceFunctionalNpcAsync return 0 } // // Panel button: place every functional NPC of the current scene (nearest first when a radius // is given, radius <= 0 = the whole scene). Anything already standing is skipped. // func GameRouter_FunctionalNpc_SpawnFunctionalNpcsAsync(ctx context.Context, session *GameSession, radius float32, includeSemantic bool) any { // Router implementation for GameRouter_FunctionalNpc_SpawnFunctionalNpcsAsync var zero any; return zero } func GameRouter_FunctionalNpc_Distance2(ctx context.Context, spot any, x float32, z float32) float32 { // Router implementation for GameRouter_FunctionalNpc_Distance2 return 0 } type FunctionalNpcSessionState struct { SyncRoot any `json:"syncRoot"` Cts *context.CancelFunc `json:"cts"` Loop *any `json:"loop"` // entityId -> the spot it was placed for. Doubles as the "already placed" set. Active map[uint64]FunctionalNpcSpot4229938 `json:"active"` // // Formwork ids whose placement has been started but not finished yet. // ⛑ Placement awaits several network writes and happens OUTSIDE the lock, so a tick that // starts while the previous batch is still in flight cannot see it in Active // and would place the same clerk again — two identical NPCs stacked on one spot. // Pending []uint32 `json:"pending"` // Scene the Active entries belong to; a change wipes them. Raid uint32 `json:"raid"` }