192 lines
9.9 KiB
Go
192 lines
9.9 KiB
Go
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"`
|
|
}
|
|
|