☰
REPENTOGON 实体存档状态向量(EntitiesSaveStateVector)Lua API 详解
2026/10/16 6:18:56 网站建设 项目流程
  • 游戏开发
  • 插件系统

【免费下载链接】REPENTOGON

Script extender for The Binding of Isaac: Repentance

项目地址:https://gitcode.com/gh_mirrors/re/REPENTOGON
点击查看免费下载

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,该类的全部公开接口只有四个:

接口签名说明
GetEntitiesSaveState Get(int Index)按下标取得一个存档状态对象
GetByTypeEntitiesSaveState GetByType(EntityType Type, int Variant = 0, int SubType = 0)按类型过滤,返回匹配状态的 Lua 表
Clearvoid Clear()清空向量中所有已登记的存档状态
__lenint __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)的语义值得注意:

    1. 它遍历容器中的每一个EntitySaveState,按st.type == type && (variant == -1 || st.variant == variant) && (subtype == -1 || st.subtype == subtype)进行过滤;
    2. 返回值不是单个对象,而是一个以 1 为起始下标的 Lua table,表中每一项都是命中条件的EntitiesSaveState用户数据(保持容器内原始下标);
    3. 没有命中时返回空表,不会报错。

    因此典型用法是配合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) end

    3.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)用于存储拾取物的ShopItemId
    GetI1/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 开头的系统说明:

    1. 为什么需要劫持:游戏原生EntitySaveState结构中没有为 Mod 预留数据空间,REPENTOGON 通过“劫持”结构中不常用的字段来存放 ESSM ID;
    2. 具体字段:对EntitySaveState而言,gridSpawnIdx被用作标记(marker),_intStorage7被用作 ID(见EntityTraits,EntitySaveStateManagement.cpp);
    3. 标记取值:HIJACK_MARKER = 0x5247、READ_MARKER = 0x5248、CLEARED_MARKER = 0x5249、WRITTEN_MARKER = 0x5246,以及调试版专用的DEBUG_CLEARED_MARKER = 0x5250;
    4. 原始值备份:被劫持字段的原始内容会复制到s_systemData.hijackedStates[id]中,在实体 Restore 前恢复,从而不破坏游戏原有逻辑;
    5. 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):

    差异点EntitiesSaveStateVectorGridEntitiesSaveStateVector
    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

    底层链路是:

    1. C++ 侧LuaFunctions::GetEntitySaveStateId读取状态的 ESSM ID(未处于劫持状态时会报错"Provided EntitiesSaveState is not supported by EntitySaveStateManager.",见 EntitySaveStateManagement.cpp);
    2. Lua 侧以modId → saveStateId → dataTable的三层映射维护数据(见entity_save_state_manager.lua的s_saveStateData);
    3. 存储/恢复/复制/清空/序列化均通过 C++ 侧的LuaCallbacks(StoreEntity、RestoreEntity、ClearStates、CopyStates、Serialize、PreDeserialize、Deserialize)自动触发,Mod 无需手动调用。

    这正是上文“Clear()会影响整个 ESSM 关联系统”的原因:EntitySaveState_ClearBatch在回收 ID 的同时会触发ClearStates回调,把对应 ID 的 Mod 数据一并删除,保证两侧数据一致。

    八、注意事项与最佳实践

    1. 索引从 0 开始:Get(i)的合法范围是0到#vec - 1,越界会直接抛 Lua 错误,遍历时务必配合__len使用。
    2. 引用而非副本:通过Get获得的EntitiesSaveState是对容器元素的实时引用,Set*修改会写回游戏内存,切勿在遍历期间随意Clear。
    3. -1通配符只作用于 Variant/SubType:GetByType的 Type 参数不支持通配,必须给出具体EntityType。
    4. GetByType返回 table:其返回结构与Get的单对象不同,是 1 起始的数组表,建议用ipairs遍历。
    5. 区分两个 Vector 的Clear语义:EntitiesSaveStateVector:Clear()会回收 ESSM ID 并触发 Lua 侧数据清理;GridEntitiesSaveStateVector:Clear()仅将_type置零。
    6. 数据自动持久化:通过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
    点击查看免费下载
    上一篇:GrapesJS 快速入门:从零搭建一个可视化页面构建器
    下一篇:Repomix FAQ 实战指南:私有仓库打包、Token 压缩、安全检测与 MCP 集成全解

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询