0-引擎

0-引擎

CET模组的共享运行时层,集中管理状态缓存、生命周期处理和事件分发,以减少冗余的引擎调用。

[center][img]https://iili.io/q32xIXj.png[/img][/center]

在开发各种CET模组的过程中,我一直反复遇到同样的模式:一遍又一遍地编写小段的基础设施代码。

生命周期处理、黑板轮询、载具状态跟踪、距离检查、存储辅助工具、日志包装器……每个模组都需要某种版本,而每个版本最终都略有不同。这些单独来看都不算繁重,但在多个项目中重复就会开始变得混乱。

0-Engine 是我最终决定不再重写这些系统,而是妥善地一次性构建它们。

它将状态缓存、事件分发、邻近检查、日志和存储集中到一个共享的运行时层,供CET模组使用。如果你只是在玩游戏,你不会注意到什么,除非你用了大量模组并且它们开始依赖这个;但如果你是编写模组的人,你很可能会注意到。

如需更深入的模组开发示例,请参阅我在可选文件下上传的 Readme.MD 文件。

[center][img]https://i.ibb.co/NddD8hKH/1-cache.png[/img][/center]

0-Engine 运行一个统一管理的更新循环,并从中构建一个中央状态快照。模组可以订阅事件和读取缓存值,而无需各自进行轮询。

状态快照包括:

  • 玩家实体(安全的生命周期处理)
  • 位置 + 朝向 + 偏航角
  • 菜单状态
  • 战斗状态
  • 载具状态
  • 衍生状态缓存(武器类型、游戏内时间、摄像机俯仰角、区域等)
  • 完整的 PlayerStateMachine 黑板(原始枚举 + 便捷标志)

通过以下方式访问:

[quote][code]local Engine = GetMod("0-Engine") local state = Engine.GetState()[/code][/quote]

返回的状态是深度只读的。如果有东西试图修改它,会立即抛出异常。

黑板值每帧都会读取,但枚举解析和事件触发仅在值发生变化时进行。原始值位于 state.blackboard.psm.* 下,便捷标志位于 locomotion/combat/body/vision/vehicle/scene 下。

衍生状态包括需要引擎调用的内容:武器类型(带记录ID缓存)、游戏内时间、玩家死亡状态、摄像机俯仰角(前向向量的反正弦,已限制)、hasWeapon(带缓存TweakDBID的TransactionSystem槽位检查)、工作点检测和区域(通过PreventionSystem解析,并约每30帧进行节流及变更检测)。

[center][img]https://i.ibb.co/2362L2dC/1-event.png[/img][/center]

玩家生命周期得到防御性处理。引擎跟踪玩家实体哈希,并区分为首次获取、失效和重新创建(重新加载/重生)。它相应地触发 PlayerReadyPlayerInvalidatedPlayerRecreated,并原子地更新其内部引用。

如果你只想要安全的初始化:

[quote][code]在你的 onInit 函数中: local Engine = GetMod("0-Engine")

Engine.WhenReady(function(player) Engine.Log("MyMod", "Player ready", "info") end)[/code][/quote]

从这里开始,预期的模型是基于转换驱动的逻辑,而不是持续的轮询:

[quote][code]Engine.Subscribe("SlidingChanged", function(isSliding) if isSliding then Engine.Log("MyMod", "Sliding started", "debug") end end)

Engine.Subscribe("DistrictChanged", function(district) Engine.Log("MyMod", "District: " .. district) end)[/code][/quote]

如果你确实需要周期性逻辑,请使用帧节流而不是每帧更新:

[quote][code]Engine.OnFrame(10, function(frame) local s = Engine.GetState() if s.inMenu then return end -- 每第10帧执行的轻量级周期逻辑 end)[/code][/quote]

事件发射器有意设计得简单且安全:监听器受 pcall 保护,迭代使用浅拷贝,因此回调期间的取消订阅不会中断分发。

[center][img]https://i.ibb.co/xqnwvbWS/1-distance.png[/img][/center]

0-Engine 提供了几种不同的距离检查变体。首先是一些更简单的,检查我们是否处于某个特定区域内,或者我们是否有一小组要检查的坐标。第二种是空间哈希,主要用于大型坐标集。

距离检查使用缓存的玩家位置。IsNear() 使用平方距离(无平方根)。区域支持每个区域的节流和安全的回调。

[quote][code]local zone = Engine.RegisterZone({ id = "afterlife_door", x = -1525.12, y = 1378.44, z = 32.0, radius = 8, throttle = 5, onEnter = function(dist) Engine.Log("MyMod", "Entered zone (" .. dist .. "m)") end, onExit = function(dist) Engine.Log("MyMod", "Exited zone (" .. dist .. "m)") end })[/code][/quote]

区域通过帧取模节流进行检查,回调被 pcall 包装,因此一个错误不会级联。

基于网格的空间索引用于大型坐标集。注册一次条目,即可获得 O(1) 的最近/范围内查询。可选的自动轮询会在玩家进入/离开/接近已注册位置时触发回调。

[quote][code]local handle = Engine.RegisterSpatialSet("my_items", { { x = 100, y = 200, z = 10, id = "item_1" }, { x = 500, y = 300, z = 15, id = "item_2" }, }, { gridSize = 100, -- 单元格大小(米)(默认 100) pollRadius = 60, -- 自动轮询半径(nil = 仅手动查询) pollThrottle = 30, -- 轮询之间的帧数(默认 30) onEnter = function(entry, distSq) print("Near:", entry.id) end, onExit = function(entry) print("Left:", entry.id) end, })[/code][/quote]

更多示例、生命周期、移除等内容,请参阅 readme.md 文件。

[center][img]https://i.ibb.co/JRphYgFk/1-npc.png[/img][/center]

很多模组需要知道特定NPC何时生成——用于服装替换、自定义对话、行为改变。标准方法是挂钩 NPCPuppet.OnGameAttached,这会对游戏中的每个NPC触发。如果每个模组都独立这样做,意味着每次会话中,每个模组都会让这个跨边界调用运行数百次。这不太好。

0-Engine 将过滤移入 RedScript。通过 TweakDBID 注册你关心的NPC,RedScript 端的哈希映射处理其余部分。不匹配的生成永远不会跨越到 CET。匹配的生成会被排队并分发到你的回调。

[quote][code]local handle = Engine.WatchNPC("Character.Panam", { onSpawn = function(entityID, recordID) Engine.Log("MyMod", "Panam spawned!") end, onDespawn = function(entityID, recordID) Engine.Log("MyMod", "Panam despawned!") end })[/code][/quote]

全局总线事件(NPCSpawned / NPCDespawned)也会触发,如果你更愿意通过这种方式订阅的话。skipCrowd 默认为 true,这样你不会收到人群重复项。

[quote][code]Engine.Subscribe("NPCSpawned", function(entityID, recordID, tdbidString) -- 为任何被监视的NPC触发 end)[/code][/quote]

当没有人监视时,每个NPC生成的开销只是一次整数比较。所以……基本上没什么开销。

[center][img]https://iili.io/q32IoVj.png[/img][/center]

累积起来的性能选择:

  • 黑板每帧只读取一次
  • 枚举名称仅在变更时解析
  • IsNear() 使用平方距离
  • 区域节流
  • 监听器 pcall 保护
  • 缓存热路径的 TweakDBID
  • 带备份恢复的原子式存储写入
  • 环形缓冲记录器(256 个条目)

每项本身并不算特别,但它们加在一起效果就显著了,所以……

[center][img]https://iili.io/q32zjMx.png[/img][/center]

此模组要求你安装以下内容: [url=https://www.nexusmods.com/cyberpunk2077/mods/107]Cyber Engine Tweaks[/url] [url=https://www.nexusmods.com/cyberpunk2077/mods/7780]Codeware[/url] [url=https://www.nexusmods.com/cyberpunk2077/mods/1511]redscript[/url]

[center][img]https://iili.io/q32IsWb.png[/img][/center]

1)安装 [url=https://www.nexusmods.com/cyberpunk2077/mods/107]Cyber Engine Tweaks[/url] 2)安装 [url=https://www.nexusmods.com/cyberpunk2077/mods/7780]Codeware[/url] 3)安装 [url=https://www.nexusmods.com/cyberpunk2077/mods/1511]redscript[/url] 4)将下载的文件解压到你的主游戏目录,或使用你的模组管理器进行安装。

[center][img]https://iili.io/q32Ip5P.png[/img][/center]

删除文件夹:\Cyberpunk 2077\bin\x64\plugins\cyber_engine_tweaks\mods\0-Engine 删除文件夹:\Cyberpunk 2077\r6\scripts\0-Engine

没有信息会写入你的存档,因此随时移除都是安全的。

[center][img]https://iili.io/q32zMue.png[/img][/center]

当前版本:0.18.6

v0.18.6 - 修复了快速存档/自动存档无法正确恢复到 isPlaying 状态的问题。问题在于 GameSession 库在恢复状态时不会触发其观察者。

v0.18.5 - 内部开发。

v0.18.4 - 内部开发。

v0.18.3 - 修复快速存档和自动存档后 SpatialHash / Proximity 条目丢失的问题

v0.18.2 - 修复载具卸载回归问题

  • 玩家失效逻辑的更改

v0.18.1 -

v0.18.0 - 弹性与性能

  • 玩家失效弹性 - 自动存档、流式区块加载和其他瞬时引擎事件不再导致错误的 playerInvalidated 触发。新的 2 秒宽限期窗口可抑制卡顿期间累积的故障;故障阈值从 0.5 秒提高到 3 秒。ForceInvalidate 仍会立即处理真实的会话拆除。
  • BlackboardCache 提前退出 - 在 Update 开始时比较所有 12 个原始 PSM 值;如果没有任何变化(约 95% 的帧),则完全跳过所有派生和事件逻辑。这是 RedScript 版本中代际计数器的 CET 原生等效实现。
  • DerivedState 节流轮询 - hasWeaponisInWorkspotisDeadcoverDirectionweaponType 现在每 6 帧(约 10Hz)轮询一次,而不是每帧。每次轮询都是一个 pcall 加上引擎系统 getter 往返。只有 lookPitch 保持每帧更新以兼容摄像机模组。
  • WhenReady 队列优化 - 跟踪计数器取代了每帧遍历所有 5 个优先级队列;批次大小从每帧 3 个回调增加到 5 个(30 个模组初始化约需 6 帧,而不是约 10 帧)。
  • 整合 pcalls - 位置+朝向合并为一个 pcall,DerivedState+BlackboardCache 合并为一个 pcall(每帧节省两次错误处理程序设置)。
  • 预分配事件负载 - MovementStateChanged 不再在每次触发时创建新表。

v0.16.1 - 加载时稳定性

  • 延迟 EngineStateSystem 初始化 - 黑板监听器现在在玩家附加后约 0.2 秒注册,让引擎在开始读取状态之前有时间完成加载。
  • 在所有 12 个黑板监听器上移除了 fireImmediately - 替换为单次 SeedStateFromBlackboard() 读取(加载期间无回调开销)。
  • 错开 NPCWatcher 重新注册(在 PlayerReady 后 0.3 秒,而非同一帧)。
  • 在分离和快速会话转换时正确清理延迟初始化。

v0.16.2 - 生命周期失效防御

  • 更具防御性的生命周期 - 增加将玩家设置为失效状态之前的等待时间。

v0.16.3 - 会话加载稳定性

  • 生命周期预热 - PlayerReady 需要连续 18 个有效帧(约 0.3 秒)才会触发,给引擎在玩家实体创建后稳定下来的时间。
  • 错开 WhenReady 分发 - 回调每帧排出 3 个,而不是一次性全部排出(将 30 个模组分散在大约 10 帧内)。
  • 将延迟初始化增加到 0.4 秒,为资源加载期间留出更多喘息空间。

v0.17.0 - 模组注册

  • Engine.Register(name) - 返回一个作用域句柄,包含所有 Engine 方法,并带有标记以进行跟踪和批量控制。
  • 作用域句柄方法: Mod.Subscribe, Mod.SetInterval, Mod.SetTimeout, Mod.SetNextTick, Mod.OnFrame, Mod.OnUpdate, Mod.WhenReady, Mod.RegisterZone, Mod.WatchNPC, Mod.RegisterSpatialSet
  • Mod.Log, Mod.SetData, Mod.GetData, Mod.ClearData 自动填充模组名称。
  • Engine.DisableMod / Engine.EnableMod - 在运行时暂停/恢复某个模组的所有回调。
  • CET 覆盖面板 - "已注册模组"部分,带每个模组的复选框切换和回调明细工具提示。
  • WhenReady 队列排出位置调整到子系统状态读取之前。

v0.17.1 - WhenReady 优先级与可靠性

  • WhenReady 优先级 - Engine.WhenReadyMod.WhenReady 接受可选的优先级(1-5,默认 3)。优先级 1 首先触发,5 最后触发。错开式排出会跨帧优先处理最低优先级。
  • 句柄自动清理 - 取消订阅或注销跟踪句柄现在会自动将其从模组的跟踪列表中移除(防止覆盖层中死句柄的积累)。
  • 增强的已注册模组覆盖层 - 工具提示现在列出每个模组订阅的具体事件名称,而不仅仅是计数。
  • 模组感知的来源标签 - 已注册模组在调试覆盖层中获得正确的归属,而不是全部显示为 "0-Engine"。
  • 11 个错误修复DrainReadyQueue 前向引用(静默杀死整个帧管道)、stateProxy 前向引用(代理缓存从未清除)、Proximity.Update 提前返回(跳过剩余区域)、一次性定时器句柄积累、EngineStateSystem 轮询弹性(轮询循环在原生方法错误时死亡)、空帧发射器清理、Mod.WhenReady 句柄泄漏、Mod.SetInterval 取消包装崩溃、Proximity.Update 缩进修复、NPCWatcher 错误归属。

v0.17.2 - NPC Watcher 会话安全性

  • NPCWatcherSystem 会话生命周期 - 现在具有与 EngineStateSystem 相同的会话保护(isReady 标志、OnPlayerAttach/OnPlayerDetachIsPreGame 保护、延迟初始化)。
  • 修复了在游戏中加载存档时的不稳定性 - 过期的监视、缓冲的生成和事件不再跨会话边界持久化。
  • 生成缓冲区捕获在 CET 重新注册监视之前加载的NPC,然后在监视到达时重放它们。
  • 来自即将结束会话的消失事件被静默丢弃(由 isReady 门控)。
  • CET 侧现在在处理事件前检查 isReady,防止过期事件泄漏。
  • 每次状态转换时增加代际计数器,以便 CET 检测跨会话边界的变化。

v0.17.3 - 会话切换稳定性

  • 轮询重新调度移至 Tick 末尾 - 旧的"先重新调度"模式产生了在会话拆除后存活的僵尸轮询,当它们遇到空系统时导致 CTD。
  • 黑板链拆分 - GetBlackboardSystem() 在会话转换期间可能返回 null;使用空值保护拆分链式调用。
  • DeferredInit 中的 IsPreGame 保护 - 防止延迟初始化在主菜单触发的冗余安全性。

完整功能列表:

  • 模组注册(Engine.Register、作用域句柄、运行时启用/禁用)
  • 带优先级级别(1-5)和跨帧错开排出的 WhenReady
  • RedScript EngineStateSystem(代际计数的 PSM 缓存)
  • RedScript NPCWatcherSystem(带完整会话生命周期的过滤NPC生成/消失跟踪)
  • 带边沿转换事件的全面 PSM 黑板缓存
  • 衍生状态系统(武器、区域、掩体、摄像机俯仰角、工作点等)
  • 空间哈希(基于网格的批量邻近查询)
  • NPC Watcher API
  • 生命周期处理(PlayerReady、PlayerInvalidated、PlayerRecreated)
  • 带节流进入/离开/滴答回调的邻近区域
  • 带 ImGui 覆盖层的集中式日志记录
  • 持久化存储层
  • 帧调度(OnUpdate、OnFrame、SetTimeout、SetInterval)
  • 带每个模组回调明细、事件列表和来源归属的 CET 覆盖层

[center][img]https://iili.io/q32T9mF.png[/img][/center]

使用了 psiberx 的库(Cron、GameSession、GameUI/GameHUD 辅助工具、Ref)和 Codeware。 感谢 CET 和 redscript 社区提供的工具和文档。 感谢 NightlyNow 提供的标题图形。