- 游戏开发
- 插件系统
【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
EntitiesSaveStateVector是 REPENTOGON 为《以撒的结合:忏悔》脚本扩展器提供的一类只读代理容器对象,它直接包装游戏内部的std::vector<EntitySaveState>,用于访问房间、玩家与关卡中保存的实体存档状态。本文将基于官方文档与仓库源码,完整讲解该类的Get、GetByType、Clear与__len四个接口的签名、语义、返回值与底层实现,并延伸介绍如何通过RoomDescriptor、Level、EntityPlayer获取此类对象,以及它与EntitySaveState、EntitySaveStateManager之间的协作关系,帮助 Mod 作者在存档管理场景中安全地读写实体状态数据。
一、背景:什么是 EntitySaveState 与存档状态向量
在《以撒》的存档机制中,游戏会把房间内的实体以“存档状态(Save State)”的形式记录下来,以便在切换房间、回溯楼层、使用回溯道具(如时之沙漏、妈妈的盒子)时恢复实体现场。REPENTOGON 在 SaveStateManagement/EntitySaveStateManagement.cpp 中实现了一套完整的存档状态扩展系统(下称 ESSM),为每个处于活动状态的存档结构分配唯一 ID,并将 Mod 自定义数据与游戏原生状态关联起来。
EntitiesSaveStateVector正是这套系统向 Lua 侧暴露的容器视图。根据 docs/docs/EntitiesSaveStateVector.md,该类的全部公开接口只有四个:
| 接口 | 签名 | 说明 |
|---|---|---|
Get | EntitiesSaveState Get(int Index) | 按下标取得一个存档状态对象 |
GetByType | EntitiesSaveState GetByType(EntityType Type, int Variant = 0, int SubType = 0) | 按类型过滤,返回匹配状态的 Lua 表 |
Clear | void Clear() | 清空向量中所有已登记的存档状态 |
__len | int __len() | 返回向量长度(支持#vec语法) |
需要强调的是,该向量是游戏内部真实容器(std::vector<EntitySaveState>*)的直接代理,而非副本。因此通过Get拿到的EntitiesSaveState对象对字段的修改会直接写回游戏内存,这一点与常见的“快照式”返回值有本质区别。
二、如何获取 EntitiesSaveStateVector 对象
该类型自身没有构造函数,只能通过其他对象的方法间接获取。仓库源码中共有以下四个入口(对应四个不同的内部容器):
1. RoomDescriptor:房间实体与装饰存档
在 repentogon/LuaInterfaces/Room/LuaRoomDescriptor.cpp 中,Lua_GetEntitiesSaveState与Lua_GetDecoSaveState分别将descriptor->SavedEntities和descriptor->SavedEffects包装为EntitiesSaveStateVector返回,对应文档 docs/docs/RoomDescriptor.md 中的两个方法:
local roomDesc = room:GetRoomDescriptor() local entityStates = roomDesc:GetEntitiesSaveState() -- 房间内实体存档 local decoStates = roomDesc:GetDecoSaveState() -- 房间内装饰类存档(SavedEffects)2. Level:我的勿忘我(Myosotis)拾取物
在 repentogon/LuaInterfaces/LuaLevel.cpp 中,Lua_GetMyosotisPickups包装了level->_myosotisPickups:
local myosotisStates = level:GetMyosotisPickups()对应文档 docs/docs/Level.md 中的GetMyosotisPickups()。
3. EntityPlayer:妈妈的盒子(Moving Box)内容
在 repentogon/LuaInterfaces/Entities/LuaEntityPlayer.cpp 中,Lua_PlayerGetMovingBoxContents包装了player->GetMovingBoxContents():
local movingBoxStates = player:GetMovingBoxContents()对应文档 docs/docs/EntityPlayer.md 中的GetMovingBoxContents()。
三、四个核心方法逐一详解
以下实现细节均可在 repentogon/LuaInterfaces/LuaEntitySaveState.cpp 的Lua_EntitiesSaveStateVectorAPI中找到对应函数。
3.1 Get(Index):按下标访问
local state = vec:Get(0) -- 取第一个存档状态- 索引从 0 开始计数(与 Lua 的 1 起始数组下标不同,注意区分)。
- 内部实现为
Lua_Get:先通过luaL_checkinteger读取索引,再进行边界检查——当index < 0或index >=>-- 取所有类型为 EntityType.ENTITY_PICKUP 的存档状态 local pickups = vec:GetByType(EntityType.ENTITY_PICKUP) -- 指定 Variant,例如只取红心(Heart 的 variant 之一) local hearts = vec:GetByType(EntityType.ENTITY_PICKUP, 10) -- 使用 -1 通配符匹配任意 Variant 和/或任意 SubType local any = vec:GetByType(EntityType.ENTITY_PICKUP, -1, -1)这是官方文档中明确强调的要点:你可以使用
-1作为通配符,匹配任意 Variant 和/或任意 SubType。参数的默认值分别为Variant = 0、SubType = 0。底层实现(
Lua_GetByType)的语义值得注意:- 它遍历容器中的每一个
EntitySaveState,按st.type == type && (variant == -1 || st.variant == variant) && (subtype == -1 || st.subtype == subtype)进行过滤; - 返回值不是单个对象,而是一个以 1 为起始下标的 Lua table,表中每一项都是命中条件的
EntitiesSaveState用户数据(保持容器内原始下标); - 没有命中时返回空表,不会报错。
因此典型用法是配合
ipairs或#遍历:local pickupStates = vec:GetByType(EntityType.ENTITY_PICKUP, -1, -1) for i, state in ipairs(pickupStates) do local t = state:GetType() local v = state:GetVariant() local s = state:GetSubType() print(i, t, v, s) end3.3 Clear():批量清空
vec:Clear()Clear并非简单调用vector->clear(),而是先调用ESSM::EntitySaveState_ClearBatch(*vector),再清空容器。其关键作用见 repentogon/SaveStateManagement/EntitySaveStateManagement.cpp:- 遍历向量中每个状态,对处于“已劫持(Hijacked)”状态的存档执行
Core::ClearState_HijackedOnly,回收其 ESSM ID(ID 进入reusableIds复用池,见IdManager::ClearId); - 收集本次被清空的所有 ID 后,通过
s_luaCallbacks.ClearStates(clearedIds)通知 Lua 侧(即EntitySaveStateManager的ClearStates回调),使与之关联的 Mod 数据同步失效; - 若同一批次内同时存在已劫持与未劫持的状态,会输出错误日志
"[ERROR] [ESSM] - EntitySaveState_ClearBatch found both hijacked and unhijacked entities"(提示数据不一致)。
由此可以推断:直接调用
Clear()会影响整个 ESSM 关联系统,它会把该容器内所有已登记存档的 Mod 数据一并清理。因此在业务中应谨慎使用,避免在遍历期间清空导致迭代器失效。3.4 __len():长度运算符
local count = #vec -- 直接使用 # 取长度 local count2 = vec:__len() -- 或显式调用__len元方法直接返回ud->data->size()。该接口对 Lua 的#运算符生效,是遍历容器最常用的辅助手段:local states = roomDesc:GetEntitiesSaveState() for i = 0, #states - 1 do local state = states:Get(i) -- ... end四、返回的 EntitiesSaveState 对象能做什么
Get/GetByType返回的EntitiesSaveState对象本身也是一个完整类,其全部读写方法见 docs/docs/EntitiesSaveState.md,实现位于 repentogon/LuaInterfaces/LuaEntitySaveState.cpp 的方法表中。按用途可归类如下:身份标识(只读与可写)
方法 语义 GetType()/SetType(t)、GetVariant()/SetVariant(v)、GetSubType()/SetSubType(s)实体类型 / 变体 / 子类型,类型使用 EntityType枚举通用存储槽(整数)
方法 字段语义(依据官方文档注释) GetI2()/SetI2(n)用于存储主动道具拾取物的充能值 GetI3()/SetI3(n)用于存储拾取物的商店价格 GetI4()/SetI4(n)用于存储拾取物的 ShopItemIdGetI1/GetI5/GetI6/GetI7/GetI8及其 Set 版本通用整数存储槽 特殊解释的存储槽(来自源码
Lua_GetI1/Lua_SetI1/Lua_GetU1/Lua_SetU1):- 当
GetType() == 4(NPC/敌人)、33(可拾取物拾取状态相关)或292时,I1槽位按float解释读写;其余类型按整数解释; U1在GetType() == 5(拾取物)时按boolean解释读写,其余类型按int8_t整数解释。
其他字段
方法 语义 GetPos()/SetPos(Vector)存档时的目标位置,返回 Vector对象GetInitSeed()、GetDropSeed()初始种子与掉落种子(只读) GetSpawnerType()、GetSpawnerVariant()生成者类型与变体(只读) GetGridSpawnIdx()网格出生索引(只读;源码中 SetGridSpawnIdx已注释禁用)GetB1()/SetB1(b)、GetB2()/SetB2(b)、GetF1()/SetF1(f)、GetF2()/SetF2(f)布尔槽位与浮点槽位 这里需要再次强调“引用视图”的底层事实:这些 Get/Set 方法全部通过
(*ud->vec)[ud->index]解引用真实容器(见 LuaEntitySaveState.h),因此Set*的修改会立即作用到游戏内部存档状态。五、底层原理:ESSM 的“劫持(Hijack)”机制
要理解
Clear()为什么需要走EntitySaveState_ClearBatch,就必须了解 ESSM 的核心设计。根据 EntitySaveStateManagement.cpp 开头的系统说明:- 为什么需要劫持:游戏原生
EntitySaveState结构中没有为 Mod 预留数据空间,REPENTOGON 通过“劫持”结构中不常用的字段来存放 ESSM ID; - 具体字段:对
EntitySaveState而言,gridSpawnIdx被用作标记(marker),_intStorage7被用作 ID(见EntityTraits,EntitySaveStateManagement.cpp); - 标记取值:
HIJACK_MARKER = 0x5247、READ_MARKER = 0x5248、CLEARED_MARKER = 0x5249、WRITTEN_MARKER = 0x5246,以及调试版专用的DEBUG_CLEARED_MARKER = 0x5250; - 原始值备份:被劫持字段的原始内容会复制到
s_systemData.hijackedStates[id]中,在实体 Restore 前恢复,从而不破坏游戏原有逻辑; - ID 复用:
IdManager::NewId优先从reusableIds复用已清空的 ID,否则递增totalIds。
这套机制也解释了为什么
GetByType的过滤可以直接读取st.type、st.variant、st.subtype——这些字段没有被劫持,始终保存真实值。六、相关容器:GridEntitiesSaveStateVector 的差异
EntitiesSaveStateVector还有一个姊妹类型GridEntitiesSaveStateVector,二者容易混淆。后者的文档见 docs/docs/GridEntitiesSaveStateVector.md,其包装的是std::vector<GridEntityDesc>(网格实体描述符),通过roomDesc:GetGridEntitiesSaveState()获取(见 LuaRoomDescriptor.cpp)。两者接口几乎一致,但存在三点关键差异(见 LuaEntitySaveState.cpp):
差异点 EntitiesSaveStateVector GridEntitiesSaveStateVector Get返回值EntitiesSaveState(带视图引用)GridEntityDesc(直接 userdata 指针)GetByType参数Type, Variant=0, SubType=0,支持 -1通配仅 Type,无通配符 Clear语义先 EntitySaveState_ClearBatch再clear()(真正清空并回收 ID)仅将每个元素的 _type置为 0,不缩小向量长度可以看到,
GridEntitiesSaveStateVector:Clear()是“逻辑清空”(保留容器大小、仅把类型标记为 0),而EntitiesSaveStateVector:Clear()是“物理清空 + ESSM ID 回收”,两者不可互换。七、与 EntitySaveStateManager 的联动:Mod 数据持久化
EntitiesSaveStateVector中每个EntitiesSaveState都可以作为EntitySaveStateManager的第二个参数,用于读写与该存档状态关联的 Mod 数据。相关文档见 docs/docs/EntitySaveStateManager.md,实现位于 repentogon/resources/scripts/repentogon_extras/entity_save_state_manager.lua。注意:
EntitySaveStateManager是全局表,其方法必须用.(点号)而不是:(冒号)调用:local data, isNew = EntitySaveStateManager.GetEntitySaveStateData(modRef, state) -- data:与该存档状态关联的 Mod 数据表 -- isNew:该表是新建的还是先前已存在的 local existing = EntitySaveStateManager.TryGetEntitySaveStateData(modRef, state) -- 仅当表已存在时返回,否则返回 nil底层链路是:
- C++ 侧
LuaFunctions::GetEntitySaveStateId读取状态的 ESSM ID(未处于劫持状态时会报错"Provided EntitiesSaveState is not supported by EntitySaveStateManager.",见 EntitySaveStateManagement.cpp); - Lua 侧以
modId → saveStateId → dataTable的三层映射维护数据(见entity_save_state_manager.lua的s_saveStateData); - 存储/恢复/复制/清空/序列化均通过 C++ 侧的
LuaCallbacks(StoreEntity、RestoreEntity、ClearStates、CopyStates、Serialize、PreDeserialize、Deserialize)自动触发,Mod 无需手动调用。
这正是上文“
Clear()会影响整个 ESSM 关联系统”的原因:EntitySaveState_ClearBatch在回收 ID 的同时会触发ClearStates回调,把对应 ID 的 Mod 数据一并删除,保证两侧数据一致。八、注意事项与最佳实践
- 索引从 0 开始:
Get(i)的合法范围是0到#vec - 1,越界会直接抛 Lua 错误,遍历时务必配合__len使用。 - 引用而非副本:通过
Get获得的EntitiesSaveState是对容器元素的实时引用,Set*修改会写回游戏内存,切勿在遍历期间随意Clear。 -1通配符只作用于 Variant/SubType:GetByType的 Type 参数不支持通配,必须给出具体EntityType。GetByType返回 table:其返回结构与Get的单对象不同,是 1 起始的数组表,建议用ipairs遍历。- 区分两个 Vector 的
Clear语义:EntitiesSaveStateVector:Clear()会回收 ESSM ID 并触发 Lua 侧数据清理;GridEntitiesSaveStateVector:Clear()仅将_type置零。 - 数据自动持久化:通过
EntitySaveStateManager关联的 Mod 数据会随游戏存档自动保存/恢复,序列化文件存放在 Mod 数据目录下的EntitySaveStateManagement/<filename>.dat(见 EntitySaveStateManagement.cpp),Mod 开发者无需自行管理生命周期。
九、小结
EntitiesSaveStateVector虽然只有四个方法,却是 REPENTOGON 存档扩展体系中连接“游戏原生存档结构”与“Mod 自定义数据”的关键桥梁。理解其容器代理的本质、GetByType的通配过滤规则、Clear的 ESSM 联动副作用,以及它与EntitiesSaveState、EntitySaveStateManager、GridEntitiesSaveStateVector的关系,是撰写安全可靠的存档管理 Mod 的前提。本文所述全部接口签名与行为均以 docs/docs/EntitiesSaveStateVector.md 及 repentogon/LuaInterfaces/LuaEntitySaveState.cpp 的实现为准,Mod 作者可直接对照源码进一步探索。赞- 游戏开发
- 插件系统
点击查看免费下载【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
项目地址:https://gitcode.com/gh_mirrors/re/REPENTOGON相关推荐
REPENTOGON 扩展文档深度解读:GridEntitiesSaveStateVector 网格实体存档状态向量完全指南
REPENTOGON 扩展文档深度解读:GridEntitiesSaveStateVector 网格实体存档状态向量完全指南 导读 GridEntitiesSa
游戏开发插件系统REPENTOGON 状态效果枚举 StatusEffect 全解:Lua 常量、底层标志与 Mod 回调实战
REPENTOGON 状态效果枚举 StatusEffect 全解:Lua 常量、底层标志与 Mod 回调实战 导读 StatusEffect 是 REPENT
游戏开发插件系统REPENTOGON 实体类继承体系图解:EntityClassDiagram 片段与 Lua 实体 API 深度解析
REPENTOGON 实体类继承体系图解:EntityClassDiagram 片段与 Lua 实体 API 深度解析 导读 本文以 REPENTOGON 官方
游戏开发插件系统
上一篇:GrapesJS 快速入门:从零搭建一个可视化页面构建器下一篇:Repomix FAQ 实战指南:私有仓库打包、Token 压缩、安全检测与 MCP 集成全解 - 它遍历容器中的每一个
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考