package routers import ( "context" "sync" "time" ) var _ = sync.Mutex{} var _ = context.Background var _ = time.Now // // Seymour 大停电 (Blackout) — 2026-09-23 新增,改编自旧版 donor 文档 Blackout_IMPLEMENTATION_RU.md。 // 核心原则(donor 原文的关键一句):服务端 запускает нативный граф клиента,不自己做灭灯。 // 我们要做的只有三件事:把控制器 Gadget 放进世界、回答它的 deadline、把 0.3 秒的干扰 Creation // 推出去。剩下的灭灯、广告牌换肤、俄罗斯方块 UI、到点收拾,全在客户端自己的图里(GraphId 36003094)。 // 与 donor 实现的两处故意差异: // 1. 不下发 Lua。donor 用 SyncDoToLuaMaster(45437825) 走 GATE 通道调 // DoHackPressClickAction + ReleaseContextEvent 去"替玩家按那个按钮"。本服务器从未实现 // GATE Lua 下发(全仓只有 MethodId 常量),为一个 arrests 尚未验证的功能先搭一整条 // Lua 通道不划算。所以这里改成:刷出控制器后什么都不做,等客户端自己触发。 // 如果 Gadgets_Hacker_Tetris 自带 HackInteract 交互点,玩家走近就能按,链路天然成立。 // 这条假设是否成立,就是本次要实测的东西 —— 见 StartBlackoutAsync 的日志判据。 // 2. 电池不无脑补满。donor 把 6/6 当成 dev 简化。但 HackerHackTypeConfig.Id=44 的 // HackCost=4,真实服每次大停电要吃掉 4 点。这里保留还原 PRIMA 的口子,但默认仍在进入能力 // 快照时满 6/6(既有行为),避免回归。希望能:把 `HackBatteryRecoverySpeed=15` 接进来做真经济。 // 三个易混淆的对象(donor §2,务必分清): // · Gadgets_Hacker_Tetris —— 大停电控制器,本文件要的就是它。 // · Gadgets_Hacker_BigScreen —— 单独可骇的屏幕/目标,跟这条链无关。 // · Creation 56860922 —— 只是 0.3 秒干扰脉冲,不是停电本体。 // // 下一个控制器 / creation 的 instanceId(落盘,见 BlackoutGadgetCounter)。 func GameRouter_Blackout_NextBlackoutId(ctx context.Context, ) uint64 { // Router implementation for GameRouter_Blackout_NextBlackoutId return 0 } // Read-only snapshot for the admin panel — no internal types cross the boundary. func GameRouter_Blackout_ProbeBlackout(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Blackout_ProbeBlackout var zero any; return zero } func GameRouter_Blackout_GetBlackoutState(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Blackout_GetBlackoutState var zero any; return zero } // Invalidates everything donor §5 lists: running flag, instance id, all waiters. // Call on scene switch and on re-entry — a stale waiter from the previous scene would let one // round's late packet release a different round. func GameRouter_Blackout_ResetBlackoutState(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_Blackout_ResetBlackoutState var zero any; return zero } func GameRouter_Blackout_CompletePending(ctx context.Context, tcs any, value any) any { // Router implementation for GameRouter_Blackout_CompletePending var zero any; return zero } // Releases any cached waiters without failing them; used when the shape changed but // readers may still be parked. func GameRouter_Blackout_FailPending(ctx context.Context, tcs any) any { // Router implementation for GameRouter_Blackout_FailPending var zero any; return zero } // ------------------------------------------------------------------ 启动序列 // // donor §6 的完整启动序列,减去 Lua 那一步。 // ⛑️ 不要在 GAME dispatcher 里 await 这个方法(donor §5 的原话):等待 gadget ACK 会 // 占住那条唯一负责读 ACK 的连接。这里本就是给 HTTP/admin 线程调用的,天然安全;将来若有人 // 想从 RPC handler 触发,必须先 Task.Run 出去。 // 返回值 Message 里带 stages,用于判定卡在哪一环: // ok waiting-for-interact = 控制器已就位且客户端 ack 了 → 现在看客户端是否自己弹交互。 // controller-ack-timeout = pathId/原生 DTO/世界未就绪,客户端没认这个物件。 // // // 面板指定的落点。给了就以它为准;否则按配置走(默认刷在广告牌上,见 // Gameplay.BlackoutControllerAtBillboard)。 // func GameRouter_Blackout_StartBlackoutAsync(ctx context.Context, session *GameSession, state any, atOverride any) any { // Router implementation for GameRouter_Blackout_StartBlackoutAsync var zero any; return zero } // 把控制器(重新)放进世界。首次下发与 re-arm 补发共用同一段,避免两处漂移。 func GameRouter_Blackout_PushBlackoutControllerAsync(ctx context.Context, session *GameSession, id uint64, at Vec3) any { // Router implementation for GameRouter_Blackout_PushBlackoutControllerAsync var zero any; return zero } // // ⭐ 「摆了控制器但站到广告牌下没有骇入点」的自愈(2026-09-24 用户反馈「有时候不起作用」)。 // 现象与判据:服务端侧每一步都成功(`[BLACKOUT] gadget ack matched=True`、 // `controller acked`),但玩家走到落点看不到骇入提示 ⇒ 说明客户端把这条 add // **丢掉了**,而服务端无从感知。已定位的两条成因: // 1. 重号:instanceId 原来是进程内计数器,服务端重启后从头发号, // 与客户端仍记得的旧 id 撞车 ⇒ 被当成"已存在"忽略(已改落盘,见 // BlackoutGadgetCounter)。 // 2. 下发时玩家不在落点附近:面板「启动」常在玩家还在几百米外时按下, // 客户端按自己的流式/AOI 规则把物件丢掉,之后玩家传送过去也不会再加载。 // ⇒ 所以在轮次真正开始前(AskStartHackerTetris 到达)周期性补发同一条 add, // 用同一个 id 和同一组坐标(幂等:重复 add 同一个 id 不会产生第二个物件)。 // 补发在 BlackoutRearmMax 次后停止,避免变成永久后台推送(hard rule 37)。 // func GameRouter_Blackout_ReArmBlackoutControllerAsync(ctx context.Context, session *GameSession, rt any) any { // Router implementation for GameRouter_Blackout_ReArmBlackoutControllerAsync var zero any; return zero } // Direct-AOI removal of our own controller (donor §9). Never touch other scene objects // here — flipping "all light sources" back on loses the scene's authored state. func GameRouter_Blackout_RemoveBlackoutControllerAsync(ctx context.Context, session *GameSession, id uint64) any { // Router implementation for GameRouter_Blackout_RemoveBlackoutControllerAsync var zero any; return zero } // ------------------------------------------------------------------ C2S 三个回合 // // AskTaskGadgetsListLoadComplete(67359333) — 客户端确认具体这个控制器加载完了。 // ⛑️ donor §6.4 的警告在这里落实:不能把任何 ACK 当成大停电确认。列表里必须包含本次 // 的 runtimeInstanceId 才放行 waiter。 // 这个 MethodId 在 Lua 导出表里是 CSharpOnly,所以布局没有 Lua 侧证据兜底。解析失败时我们 // 记日志并只回空成功 —— 表现为干净的超时,而不是把相邻字段读错。 // func GameRouter_Blackout_OnTaskGadgetsListLoadComplete(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Blackout_OnTaskGadgetsListLoadComplete var zero any; return zero } // // AskTaskPlateLoadComplete(67794218) — 可选的 plate ack。缺了不算失败(donor §6.2), // 所以这里只登记,不释放任何 waiter。 // func GameRouter_Blackout_OnTaskPlateLoadComplete(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Blackout_OnTaskPlateLoadComplete var zero any; return zero } // // AskStartHackerTetris(63521495) — 图的连锁环节跑到这里,说明客户端真的按下去了。 // ⛑️⛑️ 必须回答一个裸 u32 deadline:客户端 midToReturnMessageReader[63521495] // 无条件执行 reader:ReadUInt32()。空回包让整条链卡住;回 0 则 deadline 立刻到期, // 表现为"刚亮起来就灭了"。 // ⛑️ 别在这里 spawn 控制器 —— 这次调用本身就是它的图被触发的结果(donor §7)。 // func GameRouter_Blackout_OnAskStartHackerTetris(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Blackout_OnAskStartHackerTetris var zero any; return zero } // // ReportHackerTetrisCreation(67652382) — 这是图的进度通知,不是俄罗斯方块通关证明 // (donor §7 的标题词)。收到就去推那一下干扰脉冲。 // ⛑️ 原子占位再发送:重复 Report / 轮次外的 Report 都必须忽略,否则会出现连续脉冲。 // func GameRouter_Blackout_OnReportHackerTetrisCreation(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Blackout_OnReportHackerTetrisCreation var zero any; return zero } // // AskFinishHackerTetris(63554976) — 客户端报告本轮结束。读 u32 score; // uint.MaxValue 是被认可的"取消/无分"哨兵(donor §7)。 // func GameRouter_Blackout_OnAskFinishHackerTetris(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Blackout_OnAskFinishHackerTetris var zero any; return zero } // Per-session 大停电状态。donor §5 要求按 session + 场景代存,不能做成进程级全局。 type BlackoutRuntimeState struct { SyncRoot any `json:"syncRoot"` Gate *sync.Mutex `json:"gate"` RuntimeInstanceId uint64 `json:"runtimeInstanceId"` TaskGraphSent bool `json:"taskGraphSent"` Running bool `json:"running"` EndsUtc time.Time `json:"endsUtc"` CreationSent bool `json:"creationSent"` // 本轮控制器的落点 —— 补发(re-arm)时要用同一组坐标。 AtX float32 `json:"atX"` AtY float32 `json:"atY"` AtZ float32 `json:"atZ"` GadgetLoaded *any `json:"gadgetLoaded"` Started *any `json:"started"` Finished *any `json:"finished"` }