25 KiB
RPG 系统模块
脚本入口: eve.RPG()
组合属性、效果、状态(Buff)、技能、施法与伤害结算,并提供目标追踪器(任务/成就/教程)、 等级/经验成长、当前生命等资源(受伤/治疗/死亡)、装备加成与掉落、特征(Traits)、 回合制战斗、职业与升级学技能、以及全局开关/变量(GameState)。
ECS 组件 RPGActor::Statuses 保存运行时 buff/debuff 实例;可用 applyBuff /
hasBuff / removeBuff* 等别名,与 applyEffect / hasEffect / removeStatus* 等价。
基本用法
local rpg = eve.RPG();
local actor = rpg.newActor();
actor.setBaseAttribute("hp", 100);
actor.setBaseAttribute("attack", 12);
print(actor.getFinalAttribute("hp") + "\n");
// 注册效果后施加 buff(JSON 可带 extra 自定义字段)
rpg.registerEffectsFromJson(@"[
{""id"":""buff.power"",""durationPolicy"":""duration"",""duration"":5.0,
""stackPolicy"":""refresh"",
""modifiers"":[{""attribute"":""attack"",""op"":""add"",""value"":5.0}],
""tags"":[""buff""],""extra"":{""icon"":""ui/power.png""}}
]");
local id = actor.applyBuff("buff.power", "potion");
actor.setBuffProp(id, "uiTint", "#ffaa00");
对象关系与调用时机
RPG 保存 effect/skill 定义和结算 pipeline;RPGActor 保存属性、状态、已学技能、冷却、施法状态、
等级/经验与当前资源。JSON 是定义,Actor 是运行时实例,事件队列负责把结算连接到表现。
每帧 rpg.update(dt) 后可轮询:
- tick 事件(DOT/HOT):
getTickEvent* - 状态变更(apply/refresh/stack/remove/expire/reject):
getStatusChangeEvent* - 施法结算:
getCastEvent* - 升级:
getLevelUpEvent* - 当前资源(受伤/治疗/死亡):
getVitalsEvent*
C++ 扩展点:StatusSystem::registerApplyCondition / registerStackPolicy /
registerLifecycleHook(免疫、自定义叠层、UI 钩子)。
目标导向指南
建立角色属性
newActor() 后用 setBaseAttribute 建立 hp、attack、defense 等基础值;永久或临时加成通过 modifier 添加,最终战斗数值读取 getFinalAttribute()。
注册并释放技能
从 JSON 注册 effect 和 skill 定义,让 Actor 学习技能;释放前用 canCastSkill() 和原因接口检查资源、冷却与条件。每帧 rpg.update(dt),随后消费 cast/tick 事件并执行表现或伤害。
追踪任务与成就
local rpg = eve.RPG();
local loaded = rpg.replaceQuestsFromJson(questJson);
if (!loaded.ok) throw loaded.status.summary;
local log = rpg.newTracker(); // 玩家任务
local ach = rpg.newTracker(); // 成就(定义里 startPolicy=auto)
log.activate("quest.hunt");
log.notify("kill", "slime", 1);
ach.notify("kill", "slime", 1); // 两份 Tracker 互不影响
log.pollEvents(); // 处理完 notify 后必须 poll 才能读 getEvent*
成长与战斗落账
actor.gainXp(120.0); // 升到下一级时产生 getLevelUpEvent*
actor.setBaseAttribute("hp", 100);
actor.setCurrent("hp", 100);
actor.takeDamage("hp", 25, "slime"); // current 夹到 [0, max=hp 最终属性],归零发 death
if (actor.isDead("hp")) actor.revive("hp");// 复活回满
// 伤害数值可先走结算流水线:SettlementContext.set("amount", dmg) 后调用 takeDamage 落账
装备加成与掉落
rpg.registerItemStatsFromJson("sword", @"[
{""attribute"":""attack"",""op"":""add"",""value"":15}
]");
rpg.syncEquipModifiers(actor, equipmentSet); // 按槽位 source="equip:<slot>" 施加/撤销属性加成
rpg.registerLootTablesFromJson(@"[
{""id"":""goblin"",""entries"":[{""itemId"":""gold"",""chance"":1.0,""minQty"":5,""maxQty"":5}]}
]");
local bag = inv.newBag(20); // 或任何 inventory::Bag
local drops = rpg.rollLoot("goblin", bag, 42);
特征 / 职业 / 回合制战斗 / 全局状态
// 特征:参数倍率落到属性,元素/状态耐性、暴击等供战斗查询
rpg.registerTraitsFromJson(@"[
{""id"":""mighty"",""traits"":[{""kind"":""paramRate"",""target"":""attack"",""value"":1.5}]},
{""id"":""fire_resist"",""traits"":[{""kind"":""elementRate"",""target"":""fire"",""value"":0.5}]}
]");
actor.applyTrait("mighty", "class");
actor.getElementRate("fire"); // 1 无修正
actor.getExParam("critRate");
// 职业 + 升级学技能
rpg.registerClassesFromJson(@"[
{""id"":""warrior"",""traits"":[""mighty""],
""learnSkills"":[{""skillId"":""power_strike"",""level"":2}]}
]");
actor.setClass("warrior");
actor.gainXp(100); actor.checkLevelSkills(); // 升级后补学已解锁技能
// 回合制战斗:伤害公式 DSL "a.atk * 4 - b.def * 2";阵营任意、伤害资源任意
rpg.registerSkillDamage("fireball", "hp", "a.attack * 2", "fire", 0.0, 100);
rpg.registerSkillDamage("mana_drain", "mana", "a.attack", "", 0.0, 100); // 打到 mana
local battle = rpg.newBattle();
battle.addActor(hero, 0); battle.addActor(slime, 1); // 阵营是任意整数 id
battle.setPlayerSide(0); // 玩家侧(默认 0)
battle.setAction(hero, "fireball", slime);
battle.autoEnemyActions();
battle.startRound();
while (battle.executeNextAction() && !battle.isFinished()) {}
if (battle.isVictory()) { /* 胜利 */ }
battle.getWinnerSide(); // 多派系时判定获胜阵营
// 公式注册时严格校验;成功返回 1,失败返回 0 且保留同 id 的旧定义
// 全局开关/变量(RPG Maker 的 $gameSwitches / $gameVariables)
local gs = rpg.newGameState();
gs.switchOn("door");
gs.setVariable("gold", 100);
// 稳定的跨地图对象生命周期;权威数据仍写入 gs,因此自动进入 RPGSaveSession
local world = rpg.newWorldState(gs);
local consumed = world.consumeObject("village", "old_chest");
if (!consumed.ok) print(consumed.status.summary + "\n");
if (world.isObjectConsumed("village", "old_chest")) { /* 不再生成宝箱 */ }
伤害公式支持数字、a.<属性>、b.<属性>、括号、+ - * / 和一元正负号。
缺失括号、尾随文本、非法 token 与非有限数值会拒绝注册;C++
evaluateFormulaChecked() 在运行时除零返回结构化失败,兼容求值入口投影为 0。
常见问题
- 只注册 Skill 未注册其引用的 Effect。
- 忘记每帧
rpg.update(dt),导致冷却、DOT 和施法不推进。 - 直接修改最终属性:基础值和 modifier 应分层管理。
- 追踪器读完事件前先
pollEvents();两个 Tracker 要各自notify;completePolicy=claim时满进度是ready,要claim。 - 掉落物品需先在
inventory模块注册物品定义,否则bag.addItem拒绝加入。
API 快查
下列方法名来自当前 Squirrel 绑定;同一模块创建的辅助对象(例如 World、Body、Source)的方法也列在这里。
addAttributeModifier()、addTag()、applyEffect()、beginCastSkill()、canCastSkill()、canCastSkillReason()、cancelCastSkill()、clearEffectDefinitions()clearAllItemStats()、clearClassDefinitions()、clearItemStats()、clearLootTables()、clearQuestDefinitions()、clearSettlementPipeline()、clearSkillDamage()、clearSkillDefinitions()、forgetSkill()、gainXp()get()、getBaseAttribute()、getCastEventCaster()、getCastEventCount()、getCastEventSkillId()、getCastEventTarget()、getCastProgress()、getCastingSkillId()getCurrent()、getEffectDefinitionCount()、getFinalAttribute()、getItemStatCount()、getLevel()、getLevelUpEventActor()、getLevelUpEventCount()、getLevelUpEventNewLevel()getLevelUpEventPreviousLevel()、getLootTableCount()、getMax()、getName()、getQuestDefinitionCount()、hasQuestDefinition()、getSettlementStageCount()、getSkillCooldown()、getSkillDefinitionCount()getStatusCount()、getStatusEffectId()、getStatusInstanceId()、getStatusRemaining()、getStatusStacks()、getTickEventActor()、getTickEventCount()、getTickEventEffectId()getTickEventInstanceId()、getTickEventSource()、getTickEventStacks()、getVitalsEventAction()、getVitalsEventActor()、getVitalsEventAmount()、getVitalsEventCount()、getVitalsEventCurrent()getVitalsEventMax()、getVitalsEventResource()、getVitalsEventSource()、getXp()、getXpToNext()、has()、hasAttribute()、hasEffect()hasSettlementStage()、hasTag()、heal()、isCastingSkill()、isDead()、knowsSkill()、learnSkill()、modifyBaseAttribute()newActor()、newBattle()、newGameState()、newWorldState()、newSettlementContext()、newTracker()、registerClassesFromJson()、registerEffectsFromJson()、globalGameState()removeAllAttributeModifiersBySource()、removeAttributeModifier()、removeAttributeModifiersBySource()、removeSettlementStage()、removeStatus()、removeStatusByEffect()、removeStatusBySource()、removeStatusByTag()release()(销毁 ECS actor)、revive()、rollLoot()、runSettlement()、set()、setBaseAttribute()、setCurrent()、setSettlementStageEnabled()、setSettlementStagePriority()setSkillCooldown()、setXpToNext()、syncEquipModifiers()、takeDamage()、registerItemStatsFromJson()、registerLootTablesFromJson()、registerQuestsFromJson()、replaceQuestsFromJson()、registerSkillDamage()、registerSkillsFromJson()、registerTraitsFromJson()、clearTraitDefinitions()、getTraitDefinitionCount()、getClassDefinitionCount()、update()claimQuestRewards(tracker, gameState, bag, questId)原子领取任务声明的全部item/attribute奖励;返回结构化 Result。collectWorldLoot(...)原子提交一次持久化 loot 交互的世界消费、任务通知、属性与物品效果。settleBattleVictory(...)原子结算战斗胜利带来的经验、升级技能、任务进度、世界遭遇消费和 GameState 奖励。replaceShopOffersFromJson(json)严格验证并原子替换商品目录;buyShopOffer(...)/sellShopOffer(...)按权威 offer ID 原子交易。- 商品目录查询:
getShopOfferCount()、hasShopOffer()、getShopOfferId()、getShopOfferItemId()、getShopOfferName()、getShopOfferDescription()、getShopOfferBuyPrice()、getShopOfferSellPrice();clearShopOffers()仅用于卸载或初次发布失败回滚。 replaceEncountersFromJson(json)严格验证并原子替换遭遇目录;newEncounterMemberActor(id, index)按有序编队创建敌人,settleEncounterVictory(...)按整场定义原子结算。newEncounterActor(id)是创建首成员的单敌人兼容入口。- 遭遇目录查询:
hasEncounter()、getEncounterDisplayName()、getEncounterMemberCount()、getEncounterMemberDisplayName()、getEncounterMemberMaxHp();getEncounterMaxHp()是首成员兼容查询,clearEncounters()仅用于卸载或初次发布失败回滚。
新内容管线应使用 replaceQuestsFromJson(json)。它严格验证策略、稳定 ID、目标、奖励、依赖存在性和依赖环,
成功时一次替换完整任务目录;失败返回通用结构化 Result,且旧目录保持不变。旧的
registerQuestsFromJson 仅作为宽松兼容入口保留,不适合产品热重载。
产品任务交付应调用 claimQuestRewards(),而不是先 bag.addItem() 再 tracker.claim()。该入口先验证
任务状态、全部奖励类型、数值和背包完整批次,再提交 GameState、Tracker 与 Bag;失败不改变任一权威
所有者,也不发布库存事件。成功时库存 hook 在完整提交后执行,因此回调读取到的任务、属性和背包状态一致。
旧 Tracker.claim() 仍是无奖励状态机兼容入口,适用于成就、教程或由调用方明确自行结算的追踪器。
持久化宝箱、采集点和任务物资应调用 collectWorldLoot()。它拒绝重复消费、未激活的任务门控、
不完整通知和无法容纳的完整物品批次;成功时按同一事务更新 GameState 世界事实与属性、Tracker
目标进度和 Bag。失败时四类状态均保持原样,库存监听器只在完整提交后收到事件。
战斗结束应调用 settleBattleVictory(),不要在脚本中依次增加经验、同步职业技能、推进任务、发放
金币并消费遭遇对象。该入口先准备并验证完整成长结果,再提交 GameState、Tracker、世界事实与
RPGActor;冲突或重复遭遇不会留下部分奖励。升级事件采用提交后轮询,观察者只能看到完整终态。
产品商店应先用 replaceShopOffersFromJson() 发布完整商品目录,再调用 buyShopOffer() /
sellShopOffer();客户端只提交 offer ID 和数量,物品与买卖价格从已验证目录读取。目录拒绝未知字段、
未知物品、重复 ID、负价格和高于买价的卖价,失败时旧目录保持不变。交易会检查完整余额、背包容量
或持有数量,并检测准备后的背包陈旧冲突;失败时恢复 GameState,库存监听器只在货币与物品都到达
最终状态后收到事件。货币变量仍由 GameState 单独权威持有。
产品遭遇应先用 replaceEncountersFromJson() 发布完整目录。每个定义必须引用已注册技能和任务,并完整声明
有序 members 编队、各成员属性/技能/显示名,以及整场成长与金币奖励、任务通知、击杀计数和升级点数。
成员 ID 在一场遭遇内必须唯一,单场最多 32 名敌人。未知字段、重复 ID、悬空引用或非法数值
会拒绝整个新目录,旧目录继续服务。地图只保存稳定 encounterId;创建敌人与胜利结算都从同一个已验证定义
投影,脚本不能另传奖励金额;多成员全部被击败后仍只结算一次,从而避免地图、战斗表现和结算三份事实漂移。RPGActor 仍由 RPG ECS 世界拥有,
GameState、Tracker 和角色成长各自保持唯一权威所有者。
Tracker 方法: activate()、abandon()、canActivate()、canActivateReason()、claim()、fail()、getCount()、getEventAction()、getEventAmount()、getEventCount()、getEventEntryId()、getEventObjectiveId()、getEventReason()、getEventTarget()、getEventTopic()、getExtra()、getId()、getObjectiveCount()、getObjectiveCountRequired()、getObjectiveCurrent()、getObjectiveId()、getRewardAmount()、getRewardCount()、getRewardId()、getRewardType()、getState()、hasTag()、isObjectiveDone()、notify()、pollEvents()、reset()、syncAuto()
特征(RPGActor): applyTrait()、removeTrait()、removeTraitsBySource()、removeTraitsByTrait()、hasTrait()、getTraitCount()、getTraitIdAt()、getTraitInstanceIdAt()、getTraitSourceAt()、getParamRate()、getElementRate()、getStateRate()、isStateResist()、getExParam()、getAttackSpeed()、getAttackTimesAdd()
职业(RPGActor): setClass()、getClassId()、hasClass()、checkLevelSkills()、getClassLearnCount()、getClassLearnSkillIdAt()、getClassLearnLevelAt()
Battle: addActor()、autoEnemyActions()、executeNextAction()、getActor()、getActorCount()、getEventAction()、getEventAmount()、getEventCaster()、getEventCount()、getEventCrit()、getEventSkillId()、getEventTarget()、getPlayerSide()、getSide()、getTurn()、getWinnerSide()、isActorAlive()、isDefeat()、isFinished()、isVictory()、pollEvents()、setAction()、setPlayerSide()、startRound()
产品战斗输入应使用 setActionChecked():它只在回合开始前接受存活参战者的一次行动,并校验技能已学、
显式目标属于本场战斗且阵营符合 self / enemySingle 规则。失败返回结构化 Result 且不会污染待行动队列。
setAction() 是保留旧调用形状的兼容门面。
GameState: addVariable()、clear()、getSelfVariable()、getVariable()、hasSelfVariable()、isSwitchOn()、restoreSnapshotJson()、setSelfVariable()、setSwitch()、setVariable()、snapshotJson()、switchOff()、switchOn()
RPGWorldState: consumeObject()、isObjectConsumed()、resetObject()。这是借用 GameState 的
类型化适配器,不是第二份状态;map/object ID 必须非空、至多 256 字节且不含控制字符。consume/reset
返回结构化 Result,并以 Applied/NoOp 区分首次变化和幂等重复。GameState 必须比适配器活得更久。
成长读档: RPGActor.restoreProgression() 会校验等级、当前经验与升级阈值,并在失败时保持原成长状态不变;它不会伪造升级事件。
GameState.snapshotJson() 生成 schema 为 eve.rpg.game-state、版本为 1 的确定性 JSON;
restoreSnapshotJson() 会先校验完整候选再原子替换,未知版本或字段类型错误不会清空当前状态。
版本 1 忽略未知字段,为后续兼容扩展保留空间。
Tracker.snapshotJson() / restoreSnapshotJson() 保存任务与目标进度,不保存瞬时事件队列。
恢复时会与当前任务注册表逐项核对任务 id、目标 id、顺序和进度边界;内容版本不匹配会明确失败,
不会悄悄丢弃旧任务进度。
RPGActor.checkpointJson() / restoreCheckpointJson() 提供安全检查点语义:持久化基础属性、
等级经验、当前资源、已学技能、职业和特征身份;活动效果、施法、冷却与运行时事件属于战斗瞬态。
捕获时存在活动效果或施法会明确失败,恢复目标必须是相同职业、特征和属性布局的干净角色原型。
rpg.newParty() 创建 non-owning 的有序 RPGParty。addMember(memberId, actor) / removeMember(memberId)
以稳定成员 ID 管理关系,clear() 只清关系、不销毁 ECS 拥有的 Actor;重复 ID、重复 Actor 和非法 ID
都会返回结构化失败。count()、contains(memberId)、getMemberId(index) 提供有序 roster 查询,
getMemberActor(index) / findMemberActor(memberId) 返回即时借用引用,结构变化后不得继续保留。
hasStaleMembers() 使用 generation-qualified handle 检测已销毁 Actor;addToBattle(battle, side) 会先解析
完整 roster,只有全队有效时才一次性加入 Battle,避免半支队伍进入战斗。
recoverAtCheckpoint(healthResource, healthRatio, secondaryResource, secondaryRatio) 用于安全点恢复:它先验证
全队引用、比例和每名成员的资源上限,再统一复活并恢复可选的次要资源;任一成员无效时全队保持原状。
Battle.setActionByPolicyChecked(actor, skillId, policy) 在 canonical 行动校验前确定性选择目标。policy 支持
auto、self、lowestHealthAlly、lowestHealthEnemy;最低生命比例相同时保持参战者加入顺序。
技能的 targetType 支持 self、allySingle 和敌方目标,策略与技能目标域不一致会返回结构化失败。
队友和敌方策略可由 replaceBattleTacticsFromJson(json) 严格整批发布。每个策略是有序 rules:规则引用
skillId、targetPolicy,并可用 conditionResource+belowRatio 判断目标资源比例;最后一条必须是无条件兜底。
未知字段、未知技能、非法比例、目标域冲突或无兜底会保留上一份目录。运行时调用
queueBattleTactic(battle, actor, tacticsId) 选择首个匹配规则并通过 Battle 的 checked API 排队;
clearBattleTactics() 用于显式卸载目录。
剧情事件使用 replaceStoryEventsFromJson(json) 严格发布 schema 为 eve.rpg.story-events、版本为 1
的内容文档。每个事件包含稳定 ID、repeatable 策略和 1–128 个有序展示步骤;步骤类型为
dialogue、message、wait、move、camera,统一携带 reference、actorId、x/y 和
duration 字段,并按类型严格校验字段组合。未知 schema/version、未知字段、重复 ID、非有限坐标或
非法时长会拒绝整批替换并保留旧目录。getStoryEventCount() / hasStoryEvent(id) 查询已提交内容,
clearStoryEvents() 用于内容包补偿或卸载。
rpg.newStoryEventSession() 创建 caller-owned RPGStoryEventSession。begin(eventId, gameState) 从
GameState 的 story.event:<id> 独立变量恢复游标;非重复事件完成后再次开始会返回结构化冲突。
当前步骤通过 getStepKind()、getReference()、getActorId()、getX/Y/Duration() 作为 owning
投影交给 UI、Dialogue、Map 或 Camera adapter 执行,展示成功后才调用 advance(gameState) 确认。
advance() 还会对持久游标执行 compare-and-set 校验,两个旧 session 或错误存档 owner 不能重复确认同一步。
最后一步确认会先把完成事实写入 GameState,再令 session 结束。活动 session 拷贝不可变定义,目录热替换
不会造成悬垂引用;GameState 是游标和完成状态的唯一权威,session 不保留其指针,也不调用脚本回调。
因此游戏可在步骤确认边界保存,并在读档后用新 session 精确重放尚未确认的展示步骤。
RPGStoryEventSession 方法: advance()、begin()、getActorId()、getDuration()、
getEventId()、getReference()、getStepCount()、getStepIndex()、getStepKind()、getX()、
getY()、isActive()、isFinished()。
eve.RPGSaveSession() 是 non-owning 的存档协调器。调用
bind(gameState, tracker, actor, bag, equipment) 后,
会生成兼容的单角色 schema v1;新游戏应创建 RPGParty 并调用
bindParty(gameState, tracker, party, bag, equipment),生成按稳定成员 ID 和顺序保存全队 Actor 的 schema v2。
还必须用 setContentVersion(id) 配置当前游戏数据契约,getContentVersion() 可读取已配置值。对于只增加内容、
且当前 codec 能完整读取的旧数据版本,可逐个调用 allowCompatibleContentVersion(oldId);它不是任意数据转换器,
旧存档仍需通过当前任务、角色、物品和装备定义的全部验证。ID 确实发生变化时,使用
addIdRenameMigration(fromVersion, domain, oldId, newId) 注册直接到当前版本的纯转换;脚本 domain 支持
item、quest、questObjective、skill、class、trait、traitSource 和 equipmentSlot。
当新版本只是在 Tracker 注册表中追加任务时,使用
addQuestAdditionMigration(fromVersion, questId) 显式声明新增任务。迁移会按当前注册表顺序重建 owning
Tracker payload,只为声明的任务写入 canonical 初始状态;旧任务进度保持原值,未声明的缺失任务和已删除任务仍被拒绝。
任务定义必须在注册迁移前已加载;该规则可与同一来源版本的 ID rename 组合。
从旧单角色存档升级到队伍存档时,必须为每个来源内容版本显式调用
allowSingleActorPartyMigration(fromVersion):旧 Actor 只恢复到队伍第 0 位,其余成员保留当前内容预配置的基线,
没有登记的 schema v1 不会被队伍会话猜测性读取。
同一来源版本不能同时声明为“直接兼容”和“需要转换”,重复或冲突规则会明确失败。转换只修改通过完整性校验后的
owning payload 副本,随后仍执行全部 participant codec;磁盘原文和实时状态在 commit 前保持不变。
snapshotJson() 会生成公共严格 envelope 中的
rpg:save-session 单角色版本 1 或队伍版本 2 文档,并以 schema 固定的 128 位非密码摘要检测意外损坏;该摘要不承担防作弊或来源认证。
validateSnapshotJson(json) 会执行与恢复完全相同的 envelope、内容版本和 participant 准备流程,但不发布任何状态,
适合选择主存档或备份存档。restoreSnapshotJson() 会先验证 envelope 字段、内容摘要、精确或显式兼容的内容版本和
全部 participant,再同时发布 GameState、Tracker、玩家 Actor/全队 Actor、Bag 与 EquipmentSet,任一 participant
失败则所有权威状态都保持不变。装备属性修饰是可重建投影,不重复写入库存格式;恢复后调用
syncEquipModifiers(actor, equipment) 从权威装备状态重建。
内容版本是游戏维护的 opaque 标识,与存档 schema 版本分离;定义、任务或物品发生不兼容变更时必须
更新它并提供逐版本显式转换迁移,不能用兼容注册绕过验证。当前契约支持直接 N-1→N,不猜测缺失步骤、
不接受未知新版本且不支持降级;恢复旧版本后,下一次保存会自然写成当前版本。
使用要点
- 模块对象和它创建的资源对象应保存在全局或实体状态中,不要在每帧重复创建。
- 带
update(dt)的系统应在eve_update调用;绘制方法应在eve_render调用。 - 参数约束、默认值和返回类型以对应模块头文件及
addFunc绑定为准;本文 API 快查与当前源码同步生成。
源码: src/modules/rpg/
相关测试: 在 test/ 中搜索 rpg。