FreeMinecraftModels API 与开发者指南
生成的 Java 参考文档提供可搜索的类、方法和签名,并记录各模块的源码版本与提交。
FreeMinecraftModels 既是独立插件,也为其他插件提供 API 接口。
通用依赖和运行环境配置请参阅 Java API 索引。依赖示例使用已发布的 2.12.1,当前源码版本为 2.12.3。请使用与服务器已安装插件一致的依赖版本。
Maven 仓库
<repository>
<id>magmaguy-repo-releases</id>
<name>MagmaGuy's Repository</name>
<url>https://repo.magmaguy.com/releases</url>
</repository>
<repository>
<id>magmaguy-repo-snapshots</id>
<name>MagmaGuy's Snapshot Repository</name>
<url>https://repo.magmaguy.com/snapshots</url>
</repository>
依赖
<dependency>
<groupId>com.magmaguy</groupId>
<artifactId>FreeMinecraftModels</artifactId>
<version>2.12.1</version>
<scope>provided</scope>
</dependency>
请使用 compileOnly/provided 范围。不要将插件着色(shade)进你自己的 jar。
核心入口
ModeledEntityManager.modelExists(String)ModeledEntityManager.reload()ModeledEntityManager.getAllEntities()ModeledEntityManager.getDynamicEntities()ModeledEntityManager.propEntities()DisguiseAPI—— 将玩家伪装/解除伪装为已加载的模型LocationAPI—— 注册地下城检测器和保护提供者,供 Lua 的em.location.*谓词使用ScriptedItemAPI—— 为第三方 ItemStack 设置 FMM 物品定义的身份与显示数据
ModeledEntityManager.getAllEntities()、getDynamicEntities() 和 propEntities() 返回调用时的副本。修改返回的集合或映射不会改变 FMM 的实时注册表。检查模型是否存在时请使用 modelExists(String),无需保存或反复复制整个注册表。
核心运行时类型
ModeledEntityStaticEntityDynamicEntityPropEntity
创建实体
StaticEntity preview = StaticEntity.create("example_model", location);
DynamicEntity mobModel = DynamicEntity.create("example_model", livingEntity);
DynamicEntity mount = DynamicEntity.createWithInvisibility("example_model", livingEntity);
PropEntity prop = PropEntity.spawnPropEntity("example_model", location);
如果请求的模型 ID 未加载,所有创建路径都会返回 null。
此外,当目标方块上已经加载了同一模型的道具时,PropEntity.spawnPropEntity 也会返回 null —— 重复的道具会被拒绝而不是叠加,并会记录一条 [FMM Props] Prevented duplicate prop spawn ... 警告。请始终对它的返回值做 null 检查;非 null 的返回值是道具确实被创建出来的唯一确认。
createWithInvisibility 是一个变体,会应用隐身药水效果而不是从客户端隐藏该实体。这样实体在客户端仍受追踪,对于载具转向是必需的(被 /fmm mount 内部使用)。
实用的运行时方法
ModeledEntity#setDisplayName(String)—— 设置模型的名牌ModeledEntity#setDisplayNameVisible(boolean)—— 控制名牌是否可见ModeledEntity#setLeftClickCallback(...)ModeledEntity#setRightClickCallback(...)ModeledEntity#setHitboxContactCallback(...)ModeledEntity#setModeledEntityHitByProjectileCallback(...)ModeledEntity#playAnimation(String, boolean blend, boolean loop)—— 当名称既不匹配内置状态也不匹配模型中的动画时返回false。blend是排队而不是交叉淡入淡出;loop只对自定义动画有效。参见动画ModeledEntity#stopCurrentAnimations()ModeledEntity#hasAnimation(String)ModeledEntity#damage(double)/damage(Entity damager, double)/damage(Entity damager)/damage(Projectile)—— 经由该实体的DamageableComponent路由ModeledEntity#attack(LivingEntity)/attack(LivingEntity, double damage)ModeledEntity#teleport(Location, boolean teleportUnderlyingEntity)ModeledEntity#setUnderlyingEntity(Entity)/ModeledEntity.getModeledEntity(Entity)—— 从 Bukkit 实体反查其上附着的模型的静态方法,没有则返回nullModeledEntity#showUnderlyingEntity(Player)/hideUnderlyingEntity(Player)—— 按玩家控制底层原版实体的可见性ModeledEntity#getEntityID()—— 返回模型 ID 字符串ModeledEntity#getModelInstanceId()—— 每个实例的UUID,在该模型的生命周期内保持稳定ModeledEntity#isRemoved()/isDying()—— 生命周期标志ModeledEntity#getLocation()—— 返回当前LocationModeledEntity#getSpawnLocation()—— 返回该模型被创建时所在的LocationModeledEntity#getWorld()—— 返回WorldModeledEntity#getSkeleton()/getSkeletonBlueprint()/getMountPointManager()—— 访问运行时结构ModeledEntity#getInteractionComponent()/getHitboxComponent()/getDamageableComponent()/getAnimationComponent()—— 上述便捷方法背后的组件对象ModeledEntity.getLoadedModeledEntities()—— 包含所有已加载模型化实体的静态实时集合ModeledEntity#getViewers()—— 返回能看到该实体的玩家HashSet<UUID>ModeledEntity#getNametagBones()—— 返回名牌骨骼的List<Bone>(可用于附加额外文字)ModeledEntity#getScaleModifier()/setScaleModifier(double)ModeledEntity#removeWithDeathAnimation()—— 以死亡动画移除(若存在)ModeledEntity#removeWithMinimizedAnimation()—— 以缩小动画移除ModeledEntity#remove()—— 立刻移除实体及其所有骨骼ModeledEntity#setTintColor(Color)/getTintColor()—— 通过皮革盔甲染色通道应用持久的着色。受伤闪烁会暂时覆盖该着色,之后再淡回。传入null可清除。ModeledEntity#setViewDistanceOverride(int)/getEffectiveViewDistance()—— 为单个实体覆盖DefaultConfig.maxModelViewDistance。传入-1恢复为插件全局默认值。DynamicEntity#setSyncMovement(boolean)DynamicEntity#isDamagesOnContact()/setDamagesOnContact(boolean)—— 控制实体是否通过碰撞箱接触对玩家造成伤害DynamicEntity.isDynamicEntity(Entity)/DynamicEntity.getDynamicEntity(Entity)—— 从 Bukkit 实体出发的静态查询DynamicEntity#getBodyLocation()—— 面向身体朝向的位置,与getLocation()不同Bone#getBoneLocation()
名牌与注视目标
ModeledEntity 提供 setDisplayNameLines(List<String>)、setDisplayNameScale(float)、setDisplayNameLineGap(double)、setDisplayNameVisible(boolean) 和 getNameplateLocation()。名牌骨骼可选;没有时,名牌锚点位于缩放后的碰撞箱上方。
DynamicEntity.setLookTarget(Location) 为骨架设置视觉上的注视目标,并复制传入的位置,不会旋转或移动底层实体。传入 null 可解除。
PropEntity 专有方法
PropEntity.isPropEntity(ArmorStand)/PropEntity.getPropEntityID(ArmorStand)—— 从道具背后的盔甲架识别该道具PropEntity.hasLoadedPropOnSameBlock(String entityID, Location)——spawnPropEntity内部执行的重复检查;如果你想分支处理而不是判空,可以先调用它PropEntity.respawnPropEntityFromArmorStand(String entityID, ArmorStand)—— 围绕一个在区块重载后幸存的盔甲架重建道具PropEntity.getPropEntities()—— 以盔甲架 UUID 为键的已加载道具快照;修改它不会更改运行中的注册表PropEntity#setPersistent(boolean)—— 切换背后盔甲架的持久化PropEntity#setCustomDataString(NamespacedKey, String)/getCustomDataString(NamespacedKey)—— 在道具上读写你自己的 PDC 值。这与 Lua 的set_persistent_data/get_persistent_data辅助函数使用的是同一个存储,位于fmm_lua_<key>命名空间下PropEntity#remove()/remove(boolean showRealBlocks)—— 移除模型但保留持久化条目PropEntity#permanentlyRemove()—— 移除模型以及它的持久化条目PropEntity#setVoxelizeConfig(boolean voxelize, boolean solidify)/applySolidify()——voxelize:/solidify:YML 字段的运行时等价物PropEntity#showFakePropBlocksToPlayer(Player)/showRealBlocksToPlayer(Player)以及对应的...ToAllPlayers()变体 —— 控制那些仅存在于数据包中、为实体化道具提供客户端碰撞的屏障方块
魔法武器集成
使用魔法服务前先检查 MagicWeaponAPI.isOperational()。isWeapon(String) 检查武器定义 ID,applyWeaponData(ItemStack, String) 应用武器身份与显示数据,保留原物品的名称、描述、等级和附魔。服务已注册不代表它已能正常工作。
可选的 MagicAttackResolver 能拒绝施法、筛选目标并排序、保存发射时的战斗信息以及计算伤害。必须使用传入的一次性 MagicDamageApplication,不能直接对目标造成伤害。输入、飞行、碰撞和伤害应用由 FMM 负责。
MagicProjectileTravelEvent 是同步、可取消的碰撞查询,会检查完整移动线段,也包括命中时的检查。取消会吸收飞行物,不造成伤害或爆炸。同一线段可能被检查多次,因此监听器不能把它当作伤害通知。
事件接口
右键输入首先对真实的底层实体触发 ModeledEntityInteractEvent。它继承 PlayerInteractEntityEvent 的处理器,因此常规实体保护监听器可以取消它。取消后不会触发模型右键事件及回调。这项检查与放置、破坏方块的权限不同。
通用交互事件:
ModeledEntityLeftClickEventModeledEntityRightClickEventModeledEntityHitboxContactEventModeledEntityHitByProjectileEvent
这四个都是可取消的 Bukkit 事件;FMM 内置的检测路径会在服务器主线程上派发它们。取消其中之一会阻止 FMM 对应的回调/默认行为。碰撞箱接触扫描每两个服务器 tick 运行一次;点击事件仍有一个短暂的按玩家去重窗口,以确保数据包交互路径和 OBB 射线检测路径不会把同一个动作触发两次。
生命周期事件:
FmmReloadedEvent—— 完整初始化成功后,安排在 40 刻后于主线程触发,包括启动和成功的完整重新加载。仅重新加载导入内容的流程也会在成功时触发。准备阶段被拒绝的请求保留当前运行状态,不会发出重新加载完成事件
公开的交互接口刻意与模型类型无关。旧的 StaticEntity*Event、DynamicEntity*Event 和 PropEntity*Event 变体,以及 ResourcePackGenerationEvent,都已不再属于当前 API。请改用上面那四个通用的模型化实体事件和 FmmReloadedEvent。
持有长期 DynamicEntity 或 PropEntity 引用的消费者插件(EliteMobs、BetterStructures 等)必须处理此事件,在仍存活的底层实体上重新创建其模型附着。否则那些实体会在重载后变为不可见,因为 FMM 在 onDisable 期间已经销毁了 display 实体,而消费者的引用已经失效。
@EventHandler
public void onFmmReloaded(FmmReloadedEvent event) {
for (MyTrackedEntity tracked : myEntities) {
if (tracked.bukkitEntity != null && tracked.bukkitEntity.isValid()) {
tracked.fmmModel = DynamicEntity.create("my_model", tracked.bukkitEntity);
}
}
}
ModeledEntityHitByProjectileEvent 与 OBB 抛射物检测
FMM 对针对模型化实体的抛射物使用 OBB(定向包围盒)命中检测。当抛射物与模型化实体的 OBB 碰撞箱相交时,FMM 会触发 ModeledEntityHitByProjectileEvent。这是一个标准的可取消 Bukkit 事件。
关键细节:
- 检测会检查已跟踪位置之间的线段,以发现穿过薄模型的情况;没有前一个位置时,改用重叠检查
- 该轨迹上的候选目标按由近及远解析,与模型注册表的遍历顺序无关
- 非穿透抛射物只会被路由到一个模型化目标。带穿透的箭矢可以按飞行顺序击中
穿透等级 + 1个不同的模型化目标,并且在 OBB 和原版底层碰撞箱都观察到它时不会重复触发 - 默认的动态实体处理器会使用抛射物在撞击时刻的速度,把这次冲击作为真实的抛射物伤害事件转发给底层的生物实体。这保留了抛射物的伤害来源、战斗插件的处理逻辑以及射手归属
- 取消该事件可以阻止 FMM 的回调/默认伤害行为,但这次碰撞仍会消耗该抛射物的模型化目标额度。因此一次被取消的非穿透命中依然会消耗掉这支抛射物
@EventHandler
public void onProjectileHitModel(ModeledEntityHitByProjectileEvent event) {
ModeledEntity target = event.getModeledEntity();
Projectile projectile = event.getProjectile();
// Cancel to prevent damage
event.setCancelled(true);
}
物品与模型工具
ModelItemFactory
用于以编程方式创建模型相关 ItemStack 的工厂类。
// Create a prop placement item (uses "model_id" PDC key)
ItemStack placementItem = ModelItemFactory.createModelItem("lamp_post", Material.STICK);
// Create a custom item from config (uses "fmm_item_id" PDC key)
PropScriptConfigFields config = ItemScriptManager.getItemDefinitions().get("magic_sword");
ItemStack customItem = ModelItemFactory.createCustomItem("magic_sword", config);
createModelItem(String modelId, Material material)—— 创建道具放置物品。在 1.21.4+ 上,如果存在 display JSON 会自动应用 display 模型渲染。createCustomItem(String itemId, PropScriptConfigFields config)—— 从统一配置创建带有名称、Lore、附魔和 display 模型的自定义物品。formatModelName(String modelId)—— 工具方法,将01_em_flame_sword之类的模型 ID 转换为Flame Sword。
DisplayModelRegistry
简单的注册表,用于追踪哪些模型拥有可用的 display JSON。
// Check if a model has a display model JSON registered
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
register(String modelId)—— 注册一个模型 ID(重载时由内部调用)hasDisplayModel(String modelId)—— 检查显示模型是否已注册;格式错误的.json不会仅因文件存在就被注册getRegisteredModels()—— 返回所有已注册 display 模型 ID 的不可变Set<String>shutdown()—— 清除所有注册
ItemScriptManager
虽然保留了原类名,它现在负责物品定义和魔法武器目录,不再运行装备物品的脚本。getItemDefinitions() 返回已注册物品定义,getWeaponCatalog() 返回已验证的武器定义。旧的活动脚本、装备更新和玩家脚本清除 API 已移除。
ScriptedItemAPI
isValidItemId(String):检查精确的物品定义 ID,即去掉.yml的配置文件名。getItemConfig(String):返回定义,不存在则返回null。applyScriptedItemData(ItemStack, String):设置fmm_item_id,并在有已注册显示模型时应用该模型。物品 ID 无效或缺少物品元数据时返回false。
此方法不会改变原物品的名称、描述或附魔,也不会附加 Lua 行为。附魔请通过 MagmaCore 单独应用。弓和弩的拉弓状态属于基础显示模型定义;不要给物品 ID 添加 _idle。
ModelItemAPI
如果只需要 FMM 显示模型,请使用 ModelItemAPI.applyDisplayModel(ItemStack, String)。它检查显示模型注册表,不设置物品定义的身份信息。这与选择 FMM 武器或物品定义是不同的操作。
DisguiseAPI
玩家伪装功能的公共入口。第三方插件应调用此类,而不是内部的 DisguiseManager,以便在内部重构时保持安全。
import com.magmaguy.freeminecraftmodels.api.DisguiseAPI;
// Disguise a player as a loaded model. Replaces any existing disguise cleanly.
boolean ok = DisguiseAPI.disguise(player, "dragon");
// Undisguise (returns true if a disguise was removed).
DisguiseAPI.undisguise(player);
// Query state.
boolean disguised = DisguiseAPI.isDisguised(player);
String modelID = DisguiseAPI.getDisguiseModelID(player); // null if not disguised
// Snapshot of all currently disguised players.
Collection<Player> all = DisguiseAPI.getDisguisedPlayers();
disguise(Player, String modelID)—— 尝试替换前先移除旧伪装;未知 ID 会在移除后返回false。请传入在线玩家和准确的已加载 ID;API 不会规范化 ID 或检查命令权限undisguise(Player)—— 若有伪装被移除则返回trueisDisguised(Player)—— 快速布尔检查getDisguiseModelID(Player)—— 返回活动的模型 ID 或nullgetDisguisedPlayers()—— 不可修改的伪装玩家快照
FMM 结合玩家默认可见性、隐身药水效果和装备显示覆盖。只有本次伪装最初添加的效果被移除时才会补回;伪装前已有的效果不由其管理或恢复。命令会先验证模型再替换,与 API 不同。生命周期及客户端显示限制见玩家伪装。
LocationAPI
供插件注册地下城检测和区域保护检查的公共 API。注册的谓词用于 FMM 的 Lua 检查 em.location.is_in_dungeon 和 em.location.is_protected,例如内置的 pickupable.lua 和 storage_double.lua 脚本会使用它们。
插件传入的是简单的 Predicate<Location>,因此没有被着色的 FMM 类型跨插件类加载器。
import com.magmaguy.freeminecraftmodels.api.LocationAPI;
// On your plugin's enable, after WorldGuard/EliteMobs/etc. are available.
LocationAPI.registerDungeonLocator("EliteMobs",
location -> EliteMobs.isInsideDungeon(location));
LocationAPI.registerProtectionProvider("WorldGuard",
location -> WorldGuardBridge.isProtected(location));
registerDungeonLocator(String providerName, Predicate<Location> predicate)—— 任一注册的谓词返回true即将该位置标记为"在地下城中"registerProtectionProvider(String providerName, Predicate<Location> predicate)—— 任一注册的谓词返回true即将该位置标记为受保护
运维人员可以通过 /fmm location 来验证注册情况,该命令会报告活动的提供者数量并针对当前位置测试两个谓词。
保护提供者与道具放置
同一批保护提供者也驱动 preventPropPlacementInProtectedRegions 的道具放置检查,但该检查调用的是 canBuild(player, location),而不是仅基于位置的 isProtected(location)。这个区别很重要:
| 提供者 | canBuild 行为 |
|---|---|
| 内置 WorldGuard 适配器 | 先遵循 WorldGuard 自身的绕过设置,然后交给 WorldGuard 的 testBuild —— 因此区域成员和拥有者可以正常建造 |
| 内置 GriefPrevention 适配器 | 该位置没有领地即视为允许;在领地内则交给 GriefPrevention 对该玩家的建造权限判定 |
通过 LocationAPI.registerProtectionProvider 注册的提供者 | 回退到 !isProtected(location) —— 仅基于位置,会阻止所有人在你的谓词判定为受保护的位置建造 |
Predicate<Location> 无法表达针对具体玩家的权限。如果你的区域系统需要按玩家检查权限,请直接实现 MagmaCore 的 RegionProtectionProvider,覆盖 canBuild,并通过 LocationQueryRegistry.registerProtectionProvider 注册。
还有两个值得了解的行为:
- 适配器失败时以拒绝方式失败。 如果某个提供者在建造查询期间抛出异常,放置会被拒绝,并记录一条指明该提供者的警告。它绝不会悄悄地退化为"允许"。
- 每个已注册的提供者都必须同意。 第一个说"不"的提供者说了算;通过
LocationOwnership注册的归属提供者仍然是仅基于位置的,会阻止所有人。
物品放置先检查相邻格。体素化道具还会按四舍五入且至少一格的碰撞箱尺寸,检查旋转后的占用范围;数据包屏障体积使用另一种计算。直接调用 PropEntity.spawnPropEntity 不会执行玩家放置物品的检查。通过其他插件提供放置功能前,请阅读配置。
PropScriptConfigFields
读取模型旁边的 YAML,用于道具和物品定义。道具的 scripts: 列表会绑定独立的 Lua 脚本。指定 material: 则注册手持物品,不能再使用非空的 scripts: 列表。
Lua 脚本
FMM 通过 MagmaCore 引擎支持道具 Lua 脚本。文件放在 plugins/FreeMinecraftModels/scripts/,通过模型旁边的 YML 绑定。磁盘上的文件名必须以 .lua 结尾;配置中可包含或省略扩展名。
道具会把 scripts: 中的所有脚本绑定为独立实例。手持物品的效果通过独立的附魔提供者执行,不能使用非空的物品 scripts: 列表。
道具脚本钩子
| 钩子 | 触发时机 |
|---|---|
on_spawn | 道具被生成到世界中 |
on_game_tick | 道具存在期间的每个 tick |
on_zone_enter | 玩家进入脚本创建的受监视区域 |
on_zone_leave | 玩家离开脚本创建的受监视区域 |
on_destroy | 道具被移除 |
on_left_click | 玩家左键点击道具 |
on_right_click | 玩家右键点击道具 |
on_projectile_hit | 保留项:校验时会被接受,但在当前运行时并不会派发给道具脚本 |
物品脚本钩子
装备物品的 Lua 钩子及 context.item 已移除。物品效果请使用附魔定义。道具钩子仍受支持。
道具脚本上下文表
道具脚本会收到一个 context 表。下面是关键 API 的简要说明 —— 完整内容请参阅 Lua 道具 API。
context.prop:
model_id—— 蓝图模型名称current_location—— 道具的当前位置play_animation(name, blend, loop)—— 请求指定动画(blend 与 loop 默认为true);接受请求不保证立即播放stop_animation()—— 清空播放队列,若有idle则返回该动画;不能中断终止状态的死亡动画hurt_visual()—— 在道具上播放受伤(红色闪烁)视觉效果pickup()—— 排入一次移除道具并掉落其放置物品的操作mount(player)—— 排入一次骑乘尝试;返回true只表示玩家和骑乘管理器有效,并不表示最终分配到了座位dismount(player)—— 排入一次下马检查;返回true只表示玩家和骑乘管理器有效get_passengers()—— 返回当前骑乘该道具的玩家列表spawn_elitemobs_boss(filename, x, y, z)—— 在道具当前所在世界的绝对坐标处生成一个 EliteMobs Boss
context.event:
- 在道具的
on_left_click、on_right_click、on_zone_enter和on_zone_leave中可用 - 当底层钩子可取消时,可使用
cancel()、uncancel()、is_cancelled player—— 触发该事件的玩家
context.world:
spawn_entity(entity_type, x, y, z)—— 生成一个原版实体;实体类型无效时返回nilset_block_at(x, y, z, material)—— 当材质有效时排入一次方块更改;未加载的区块会被跳过- 此外还有粒子、声音、方块查询、闪电以及附近实体查找
context.cooldowns:
check_local(key, ticks)—— 检查并启动一个按脚本计的冷却global_ready()/set_global(ticks)—— 道具共享的冷却
玩家对象(来自 context.player 或 context.event.player):
get_held_item()—— 返回玩家手持的物品;type是大写的 Bukkit 材质名称is_holding_elitemobs_item(filename):检查主手物品的 EliteMobs 文件名标签,例如shroomwhispers_shears.yml。显示名称和翻译不会影响此检查。空手、物品不匹配或 EliteMobs 集成不可用时返回false。consume_held_item()—— 安排移除一个主手物品,以任务执行时的主手内容为准;不返回付款结果has_item(material)—— 检查玩家是否拥有某物品send_message(text)—— 向玩家发送聊天消息game_mode—— 玩家当前的游戏模式
使用文件名标识 EliteMobs 任务物品:
if not player:is_holding_elitemobs_item("shroomwhispers_shears.yml") then
return
end
此方法由 FMM 的 EliteMobs 集成提供。重命名普通物品不会为其添加所需的文件名标签。
道具脚本示例
return {
api_version = 1,
on_spawn = function(context)
context.prop:play_animation("idle", true, true)
end,
on_right_click = function(context)
if context.cooldowns:check_local("activate", 40) then
context.prop:play_animation("activate", false, false)
end
end
}
物品脚本示例
旧的装备物品示例已不受支持。请参阅魔法武器和附魔定义,使用当前系统创建物品。
说明
- FreeMinecraftModels 是作为已安装插件的依赖,而不是可嵌入的库。
- 如果你的插件需要刚导入的模型,请调用
ModeledEntityManager.reload(),而不要尝试自行重建 FreeMinecraftModels 的状态。 - 当前 FMM 源码依赖 MagmaCore
2.2.0-SNAPSHOT,由其提供共享 Lua 引擎、LocationQueryRegistry和WorldFolderResolver。 - FreeMinecraftModels 将 WorldGuard、WorldEdit、GriefPrevention、Vault、floodgate 和 Geyser-Spigot 声明为
softdepend。WorldGuard 与 GriefPrevention 提供内置的玩家保护适配器;WorldEdit 是 WorldGuard 的依赖项,不是独立的 FMM 保护适配器。商店需要 Vault、经济提供者及已启用的商店配置。Bedrock 显示也需要兼容的资源包与集成路径。 ModeledEntityManager.reload()请求与/fmm reload相同的异步准备和完整生命周期,不会等待完成。请从主线程调用,并以成功的生命周期反馈确认结果,而不是void返回值。无效的候选内容可能导致重新加载被拒绝,同时保留运行中的状态。