modwright 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +212 -0
- package/assets/claude/agents/mw-explore.md +46 -0
- package/assets/claude/agents/mw-fixture.md +33 -0
- package/assets/claude/agents/mw-rule-author.md +51 -0
- package/assets/claude/agents/mw-scaffold-author.md +48 -0
- package/assets/claude/skills/mw-author.md +59 -0
- package/assets/claude/skills/mw-new-item.md +51 -0
- package/assets/claude/skills/mw-new-spell.md +50 -0
- package/assets/claude/skills/mw-scaffold.md +59 -0
- package/assets/claude/skills/mw-ship.md +57 -0
- package/assets/claude/skills/mw-validate.md +51 -0
- package/assets/claude/skills/mw-verify.md +59 -0
- package/assets/nivalisnights/extract_resources.py +177 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
- package/bridges/bg3-se/README.md +99 -0
- package/bridges/bg3-se/bridge.json +68 -0
- package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
- package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
- package/bridges/cp2077-cet/README.md +165 -0
- package/bridges/cp2077-cet/bridge.json +53 -0
- package/dist/core/bridge/arm.js +175 -0
- package/dist/core/bridge/cet-console.js +26 -0
- package/dist/core/bridge/cet-log.js +362 -0
- package/dist/core/bridge/index.js +11 -0
- package/dist/core/bridge/locate.js +35 -0
- package/dist/core/bridge/paths.js +34 -0
- package/dist/core/bridge/pending.js +89 -0
- package/dist/core/bridge/poll.js +155 -0
- package/dist/core/bridge/stage.js +100 -0
- package/dist/core/bridge/status.js +97 -0
- package/dist/core/bridge/types.js +145 -0
- package/dist/core/bridge/validate.js +107 -0
- package/dist/core/build/index.js +6 -0
- package/dist/core/build/manifest.js +47 -0
- package/dist/core/build/paths.js +32 -0
- package/dist/core/build/run.js +342 -0
- package/dist/core/build/stage.js +265 -0
- package/dist/core/build/steps/bg3.js +129 -0
- package/dist/core/build/steps/core.js +195 -0
- package/dist/core/build/steps/cyberpunk.js +95 -0
- package/dist/core/build/steps/listing.js +35 -0
- package/dist/core/build/types.js +13 -0
- package/dist/core/build/zip.js +148 -0
- package/dist/core/compat/check.js +113 -0
- package/dist/core/compat/pe.js +220 -0
- package/dist/core/compat/types.js +1 -0
- package/dist/core/compat/version.js +70 -0
- package/dist/core/deploy/guards.js +292 -0
- package/dist/core/deploy/index.js +5 -0
- package/dist/core/deploy/plan.js +696 -0
- package/dist/core/deploy/reload-apply.js +361 -0
- package/dist/core/deploy/reload.js +358 -0
- package/dist/core/deploy/sources.js +169 -0
- package/dist/core/deploy/types.js +1 -0
- package/dist/core/epic.js +54 -0
- package/dist/core/fsutil.js +191 -0
- package/dist/core/home.js +70 -0
- package/dist/core/knowledge/facts.js +229 -0
- package/dist/core/knowledge/index.js +4 -0
- package/dist/core/knowledge/search.js +112 -0
- package/dist/core/knowledge/store.js +123 -0
- package/dist/core/knowledge/types.js +1 -0
- package/dist/core/ledger/check.js +42 -0
- package/dist/core/ledger/hooks.js +80 -0
- package/dist/core/ledger/index.js +7 -0
- package/dist/core/ledger/query.js +17 -0
- package/dist/core/ledger/record.js +84 -0
- package/dist/core/ledger/status.js +53 -0
- package/dist/core/ledger/store.js +98 -0
- package/dist/core/ledger/types.js +108 -0
- package/dist/core/logs/index.js +4 -0
- package/dist/core/logs/registry.js +50 -0
- package/dist/core/logs/scan.js +195 -0
- package/dist/core/logs/triage.js +160 -0
- package/dist/core/logs/types.js +1 -0
- package/dist/core/lsx.js +10 -0
- package/dist/core/lz4.js +66 -0
- package/dist/core/mods.js +229 -0
- package/dist/core/process.js +49 -0
- package/dist/core/project/build-steps.js +83 -0
- package/dist/core/project/describe.js +74 -0
- package/dist/core/project/index.js +5 -0
- package/dist/core/project/load.js +146 -0
- package/dist/core/project/schema.js +264 -0
- package/dist/core/project/types.js +3 -0
- package/dist/core/prompts.js +72 -0
- package/dist/core/registry.js +35 -0
- package/dist/core/safety/backup.js +206 -0
- package/dist/core/safety/execute.js +188 -0
- package/dist/core/safety/index.js +5 -0
- package/dist/core/safety/plan.js +122 -0
- package/dist/core/safety/restore.js +172 -0
- package/dist/core/safety/types.js +1 -0
- package/dist/core/scaffold/bootstrap.js +7 -0
- package/dist/core/scaffold/catalog.js +28 -0
- package/dist/core/scaffold/claude-assets.js +271 -0
- package/dist/core/scaffold/collisions.js +64 -0
- package/dist/core/scaffold/emit.js +356 -0
- package/dist/core/scaffold/engine.js +680 -0
- package/dist/core/scaffold/ids.js +78 -0
- package/dist/core/scaffold/index.js +10 -0
- package/dist/core/scaffold/jsonpath.js +121 -0
- package/dist/core/scaffold/load.js +81 -0
- package/dist/core/scaffold/render.js +85 -0
- package/dist/core/scaffold/sequence.js +396 -0
- package/dist/core/scaffold/types.js +309 -0
- package/dist/core/steam.js +63 -0
- package/dist/core/surfaceutil.js +36 -0
- package/dist/core/testplan/check.js +67 -0
- package/dist/core/testplan/index.js +5 -0
- package/dist/core/testplan/load.js +100 -0
- package/dist/core/testplan/registry.js +317 -0
- package/dist/core/testplan/run.js +389 -0
- package/dist/core/testplan/types.js +82 -0
- package/dist/core/text/converters/bg3.js +50 -0
- package/dist/core/text/converters/cyberpunk.js +36 -0
- package/dist/core/text/converters/index.js +261 -0
- package/dist/core/text/diff/cr2w.js +180 -0
- package/dist/core/text/diff/index.js +16 -0
- package/dist/core/text/diff/loca.js +43 -0
- package/dist/core/text/diff/lsx.js +126 -0
- package/dist/core/text/handles.js +86 -0
- package/dist/core/text/index.js +6 -0
- package/dist/core/text/json.js +46 -0
- package/dist/core/text/templates/cache.js +71 -0
- package/dist/core/text/templates/index.js +3 -0
- package/dist/core/text/templates/library.js +245 -0
- package/dist/core/text/templates/types.js +51 -0
- package/dist/core/text/toolchain.js +16 -0
- package/dist/core/text/types.js +1 -0
- package/dist/core/text/validate.js +149 -0
- package/dist/core/text/xml.js +58 -0
- package/dist/core/toolchain/datafetch.js +151 -0
- package/dist/core/toolchain/env.js +63 -0
- package/dist/core/toolchain/features.js +170 -0
- package/dist/core/toolchain/index.js +8 -0
- package/dist/core/toolchain/install.js +191 -0
- package/dist/core/toolchain/locate.js +275 -0
- package/dist/core/toolchain/remember.js +84 -0
- package/dist/core/toolchain/run.js +318 -0
- package/dist/core/toolchain/specs.js +351 -0
- package/dist/core/toolchain/types.js +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/userconfig.js +66 -0
- package/dist/core/validate/context.js +139 -0
- package/dist/core/validate/index.js +5 -0
- package/dist/core/validate/registry.js +58 -0
- package/dist/core/validate/run.js +131 -0
- package/dist/core/validate/suppressions.js +91 -0
- package/dist/core/validate/types.js +2 -0
- package/dist/core/yamledit.js +51 -0
- package/dist/index.js +389 -0
- package/dist/server.js +1914 -0
- package/dist/surfaces/baldursgate3/compat.js +164 -0
- package/dist/surfaces/baldursgate3/identity.js +110 -0
- package/dist/surfaces/baldursgate3/index/build.js +331 -0
- package/dist/surfaces/baldursgate3/index/index.js +17 -0
- package/dist/surfaces/baldursgate3/index/info.js +40 -0
- package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
- package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
- package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
- package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
- package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
- package/dist/surfaces/baldursgate3/index/queries.js +327 -0
- package/dist/surfaces/baldursgate3/index/schema.js +62 -0
- package/dist/surfaces/baldursgate3/index.js +3 -0
- package/dist/surfaces/baldursgate3/logs.js +278 -0
- package/dist/surfaces/baldursgate3/modsettings.js +115 -0
- package/dist/surfaces/baldursgate3/pak.js +153 -0
- package/dist/surfaces/baldursgate3/settings.js +49 -0
- package/dist/surfaces/baldursgate3/surface.js +232 -0
- package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
- package/dist/surfaces/baldursgate3/validators/files.js +476 -0
- package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
- package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
- package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
- package/dist/surfaces/baldursgate3/validators/story.js +790 -0
- package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/compat.js +223 -0
- package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
- package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
- package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
- package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
- package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
- package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
- package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
- package/dist/surfaces/cyberpunk2077/index.js +3 -0
- package/dist/surfaces/cyberpunk2077/logs.js +742 -0
- package/dist/surfaces/cyberpunk2077/paths.js +12 -0
- package/dist/surfaces/cyberpunk2077/surface.js +218 -0
- package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
- package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
- package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
- package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
- package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
- package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
- package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
- package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
- package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
- package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
- package/dist/surfaces/eldenring/binders.js +111 -0
- package/dist/surfaces/eldenring/compat.js +166 -0
- package/dist/surfaces/eldenring/dcx.js +49 -0
- package/dist/surfaces/eldenring/index.js +13 -0
- package/dist/surfaces/eldenring/loaders.js +132 -0
- package/dist/surfaces/eldenring/logs.js +67 -0
- package/dist/surfaces/eldenring/paramdef.js +256 -0
- package/dist/surfaces/eldenring/params.js +129 -0
- package/dist/surfaces/eldenring/surface.js +121 -0
- package/dist/surfaces/eldenring/tools.js +52 -0
- package/dist/surfaces/eldenring/validators/binders.js +193 -0
- package/dist/surfaces/eldenring/validators/packages.js +159 -0
- package/dist/surfaces/eldenring/validators/params.js +334 -0
- package/dist/surfaces/eldenring/validators/profiles.js +287 -0
- package/dist/surfaces/index.js +34 -0
- package/dist/surfaces/nivalisnights/compat.js +229 -0
- package/dist/surfaces/nivalisnights/index/build.js +310 -0
- package/dist/surfaces/nivalisnights/index/index.js +5 -0
- package/dist/surfaces/nivalisnights/index/queries.js +231 -0
- package/dist/surfaces/nivalisnights/index.js +4 -0
- package/dist/surfaces/nivalisnights/logs.js +473 -0
- package/dist/surfaces/nivalisnights/surface.js +195 -0
- package/dist/surfaces/skyrimse/archives.js +229 -0
- package/dist/surfaces/skyrimse/compat.js +163 -0
- package/dist/surfaces/skyrimse/index.js +12 -0
- package/dist/surfaces/skyrimse/loadorder.js +174 -0
- package/dist/surfaces/skyrimse/logs.js +241 -0
- package/dist/surfaces/skyrimse/plugins.js +149 -0
- package/dist/surfaces/skyrimse/records.js +94 -0
- package/dist/surfaces/skyrimse/surface.js +240 -0
- package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
- package/dist/surfaces/skyrimse/validators/archives.js +110 -0
- package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
- package/dist/surfaces/skyrimse/validators/records.js +117 -0
- package/dist/surfaces/stardewvalley/compat.js +190 -0
- package/dist/surfaces/stardewvalley/config.js +42 -0
- package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
- package/dist/surfaces/stardewvalley/index/index.js +229 -0
- package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
- package/dist/surfaces/stardewvalley/index.js +12 -0
- package/dist/surfaces/stardewvalley/logs.js +307 -0
- package/dist/surfaces/stardewvalley/mods.js +182 -0
- package/dist/surfaces/stardewvalley/surface.js +244 -0
- package/dist/surfaces/stardewvalley/validators/content.js +321 -0
- package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
- package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
- package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
- package/dist/surfaces/subnautica2/compat.js +138 -0
- package/dist/surfaces/subnautica2/index.js +5 -0
- package/dist/surfaces/subnautica2/logs.js +129 -0
- package/dist/surfaces/subnautica2/paks.js +173 -0
- package/dist/surfaces/subnautica2/surface.js +151 -0
- package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
- package/dist/surfaces/valheim/compat.js +171 -0
- package/dist/surfaces/valheim/index/index.js +301 -0
- package/dist/surfaces/valheim/index/tables.js +103 -0
- package/dist/surfaces/valheim/index.js +11 -0
- package/dist/surfaces/valheim/logs.js +197 -0
- package/dist/surfaces/valheim/packaging.js +131 -0
- package/dist/surfaces/valheim/plugins.js +270 -0
- package/dist/surfaces/valheim/references.js +121 -0
- package/dist/surfaces/valheim/surface.js +223 -0
- package/dist/surfaces/valheim/validators/packaging.js +218 -0
- package/dist/surfaces/valheim/validators/plugins.js +287 -0
- package/dist/surfaces/valheim/validators/references.js +169 -0
- package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
- package/knowledge/baldursgate3/audio.voice.yaml +182 -0
- package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
- package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
- package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
- package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
- package/knowledge/baldursgate3/crash.causes.yaml +90 -0
- package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
- package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
- package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
- package/knowledge/baldursgate3/dialogue.yaml +122 -0
- package/knowledge/baldursgate3/feats.classes.yaml +164 -0
- package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
- package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
- package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
- package/knowledge/baldursgate3/journal.quests.yaml +214 -0
- package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
- package/knowledge/baldursgate3/localization.handles.yaml +92 -0
- package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
- package/knowledge/baldursgate3/origins.companions.yaml +144 -0
- package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
- package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
- package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
- package/knowledge/baldursgate3/process.launch.yaml +34 -0
- package/knowledge/baldursgate3/progressions.yaml +92 -0
- package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
- package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
- package/knowledge/baldursgate3/races.progression.yaml +228 -0
- package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
- package/knowledge/baldursgate3/scriptextender.yaml +252 -0
- package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
- package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
- package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
- package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
- package/knowledge/baldursgate3/stats.spells.yaml +293 -0
- package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
- package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
- package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
- package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
- package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
- package/knowledge/baldursgate3/ui.icons.yaml +412 -0
- package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
- package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
- package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
- package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
- package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
- package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
- package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
- package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
- package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
- package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
- package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
- package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
- package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
- package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
- package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
- package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
- package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
- package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
- package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
- package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
- package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
- package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
- package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
- package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
- package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
- package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
- package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
- package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
- package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
- package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
- package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
- package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
- package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
- package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
- package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
- package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
- package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
- package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
- package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
- package/knowledge/cyberpunk2077/player.control.yaml +918 -0
- package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
- package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
- package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
- package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
- package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
- package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
- package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
- package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
- package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
- package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
- package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
- package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
- package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
- package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
- package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
- package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
- package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
- package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
- package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
- package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
- package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
- package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
- package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
- package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
- package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
- package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
- package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
- package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
- package/knowledge/eldenring/formats.binders.yaml +237 -0
- package/knowledge/eldenring/launcher.me3.yaml +230 -0
- package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
- package/knowledge/eldenring/params.paramdef.yaml +149 -0
- package/knowledge/eldenring/params.regulation.yaml +142 -0
- package/knowledge/eldenring/saves.online.yaml +118 -0
- package/knowledge/eldenring/toolchain.cli.yaml +256 -0
- package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
- package/knowledge/skyrimse/archives.bsa.yaml +212 -0
- package/knowledge/skyrimse/install.detection.yaml +41 -0
- package/knowledge/skyrimse/loadorder.files.yaml +242 -0
- package/knowledge/skyrimse/plugins.esl.yaml +189 -0
- package/knowledge/skyrimse/plugins.format.yaml +224 -0
- package/knowledge/skyrimse/skse.runtime.yaml +283 -0
- package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
- package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
- package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
- package/knowledge/stardewvalley/game.versions.yaml +202 -0
- package/knowledge/stardewvalley/install.layout.yaml +277 -0
- package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
- package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
- package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
- package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
- package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
- package/knowledge/subnautica2/game.build.yaml +331 -0
- package/knowledge/subnautica2/modding.state.yaml +210 -0
- package/knowledge/subnautica2/ue5.containers.yaml +72 -0
- package/knowledge/subnautica2/ue5.paks.yaml +203 -0
- package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
- package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
- package/knowledge/valheim/bepinex.loader.yaml +247 -0
- package/knowledge/valheim/bepinex.plugins.yaml +251 -0
- package/knowledge/valheim/game.versions.yaml +321 -0
- package/knowledge/valheim/install.layout.yaml +216 -0
- package/knowledge/valheim/jotunn.library.yaml +334 -0
- package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
- package/knowledge/valheim/toolchain.build.yaml +111 -0
- package/package.json +29 -0
- package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
- package/scaffolds/baldursgate3/new-item.yaml +264 -0
- package/scaffolds/baldursgate3/new-mod.yaml +174 -0
- package/scaffolds/baldursgate3/new-passive.yaml +100 -0
- package/scaffolds/baldursgate3/new-project.yaml +118 -0
- package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
- package/scaffolds/baldursgate3/new-spell.yaml +102 -0
- package/scaffolds/baldursgate3/new-status.yaml +119 -0
- package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
- package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
- package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
- package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
- package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
- package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
- package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
- package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
- package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
- package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
- package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
- package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
- package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
- package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
- package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
- package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
- package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
- package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
- package/scaffolds/stardewvalley/new-project.yaml +184 -0
- package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
- package/scripts/mw-tool.mjs +184 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
-- MODWRIGHT-BRIDGE-DO-NOT-SHIP
|
|
2
|
+
--
|
|
3
|
+
-- ModWrightBridge / probes.lua — the CET dispatch table. One entry per row of
|
|
4
|
+
-- the probe catalogue, keyed by the row's `id`, carrying that row's
|
|
5
|
+
-- EXACT status word, and nothing else: probes are data rather than new Lua,
|
|
6
|
+
-- so a test plan can never ship fresh script into the game.
|
|
7
|
+
--
|
|
8
|
+
-- ============================================================================
|
|
9
|
+
-- THE RULE THAT SHAPES EVERY HANDLER HERE
|
|
10
|
+
-- ============================================================================
|
|
11
|
+
-- The catalogue marks half these rows `unverified`, and an `unverified` row is one where NOTHING ON
|
|
12
|
+
-- DISK attests the call. So a handler whose call is missing returns `error` with a message
|
|
13
|
+
-- naming what was missing and which check settles it. It never returns `pass`, and
|
|
14
|
+
-- it never lets the absence propagate as a Lua error into CET's onUpdate.
|
|
15
|
+
--
|
|
16
|
+
-- Attestation words used below:
|
|
17
|
+
-- attested(engine) a shipped CET mod on disk makes this exact call
|
|
18
|
+
-- attested(doc) a CyberEngineTweaks wiki page names this exact call
|
|
19
|
+
-- unverified no page and no mod on disk makes it; a console check confirms or contradicts it
|
|
20
|
+
-- not designed the catalogue's `cp2077.loot.roll`: answered `unsupported`, always
|
|
21
|
+
-- Each is copied into the probe result's `evidence.attestation` with its citation, so the
|
|
22
|
+
-- provenance of an answer travels with the answer.
|
|
23
|
+
--
|
|
24
|
+
-- ============================================================================
|
|
25
|
+
-- Engine calls used in this file (source quoted above each first use)
|
|
26
|
+
-- ============================================================================
|
|
27
|
+
-- Game.GetPlayer() wiki/Script-tutorial.md §"Regular handles";
|
|
28
|
+
-- shipped CET mods
|
|
29
|
+
-- Game.GetTransactionSystem():GetItemInSlot(...) shipped CET mods
|
|
30
|
+
-- TweakDBID.new(name) shipped CET mods
|
|
31
|
+
-- ItemID.GetTDBID(w:GetItemData():GetID()) shipped CET mods
|
|
32
|
+
-- Game.GetDynamicEntitySystem():GetTaggedIDs(t) shipped CET mods
|
|
33
|
+
-- Game.FindEntityByID(id) shipped CET mods
|
|
34
|
+
-- ent:FindComponentByName(name) shipped CET mods
|
|
35
|
+
-- ent:GetWorldPosition() / GetWorldOrientation() shipped CET mods
|
|
36
|
+
-- Game.AddToInventory(item, qty) wiki/Console.md §"Specificity";
|
|
37
|
+
-- wiki/ConsoleCommands.md:25
|
|
38
|
+
-- GetMod("ModWrightBridge") knowledge fact cp2077.process.cet-mods-sandboxed
|
|
39
|
+
-- (verified, 2.31, observed in game 2026-08-16)
|
|
40
|
+
-- TweakDB:GetRecord / TweakDB:GetFlat observed in the CET console on
|
|
41
|
+
-- 2026-09-08 (see the rows below).
|
|
42
|
+
--
|
|
43
|
+
-- MUTATION. Two rows have effect `mutate`: `cp2077.inventory.add`, and `cp2077.lua.eval`
|
|
44
|
+
-- (added 2026-09-11). They are the two handlers in this
|
|
45
|
+
-- file with `mutates = true`, and `Probes.run` refuses to call a mutating handler unless the
|
|
46
|
+
-- request set `allowMutate: true` AND the kind is one of those two ids. Because the gate keys
|
|
47
|
+
-- off `mutates` and not off the request's own effect word, a request that mislabelled either
|
|
48
|
+
-- kind as `read` is refused just the same. Every other handler reads.
|
|
49
|
+
|
|
50
|
+
local Probes = {}
|
|
51
|
+
|
|
52
|
+
--- Wired by init.lua so this module needs no loader of its own.
|
|
53
|
+
local Protocol = nil
|
|
54
|
+
|
|
55
|
+
function Probes.init(protocol)
|
|
56
|
+
Protocol = protocol
|
|
57
|
+
return Probes
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
-- ---------------------------------------------------------------------------
|
|
61
|
+
-- Shared helpers
|
|
62
|
+
-- ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
local function params(probe)
|
|
65
|
+
if type(probe.params) == "table" then return probe.params end
|
|
66
|
+
return {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local function ok(observed, evidence)
|
|
70
|
+
return { status = "pass", observed = observed, evidence = evidence or {} }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local function failed(msg, observed, evidence)
|
|
74
|
+
return { status = "fail", observed = observed, evidence = evidence or {}, error = msg }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
local function errored(msg, evidence)
|
|
78
|
+
return { status = "error", observed = nil, evidence = evidence or {}, error = msg }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
--- The one message shape every `unverified` row uses when its call is simply not there.
|
|
82
|
+
local function absent(what, confirmStep)
|
|
83
|
+
return errored(what .. " is not available in this CET sandbox, and no source on disk attests it. " ..
|
|
84
|
+
"This row stays `unverified` until " .. confirmStep .. " confirms or contradicts it; it is reported " ..
|
|
85
|
+
"as an error rather than a pass so nothing downstream reads a missing call as a measurement.")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
--- An attested call whose global is nonetheless not visible from this sandbox. Distinct
|
|
89
|
+
--- from `absent`: the call is known to work (seen from the console, 2026-09-08), so a
|
|
90
|
+
--- missing global here is an environment difference to report, never a reason to call
|
|
91
|
+
--- the row unverified. The old `absent` wording was once read as a
|
|
92
|
+
--- refusal and re-verified by hand; the evidence now names the type seen.
|
|
93
|
+
local function missingAttested(what, call, evidence)
|
|
94
|
+
evidence = evidence or {}
|
|
95
|
+
evidence.globalType = what
|
|
96
|
+
return errored(what .. " is not visible from this mod sandbox, although " .. call ..
|
|
97
|
+
" is attested from the CET console (2026-09-08). Record the type seen here and compare " ..
|
|
98
|
+
"with a direct console call in the same session; this is an environment finding, not an " ..
|
|
99
|
+
"unverified probe.", evidence)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
--- Is a global present, without calling it?
|
|
103
|
+
-- Globals are read by NAME through a closure, never through `_G`: a CET mod's environment
|
|
104
|
+
-- is a fresh table falling back to a whitelisted globals copy without `_G`
|
|
105
|
+
-- (CyberEngineTweaks/src/scripting/Sandbox.cpp:9, LuaSandbox.cpp whitelist), so `_G[name]`
|
|
106
|
+
-- is always nil in here. Observed in game: the heartbeat, which detects by
|
|
107
|
+
-- name, said GetMod true while this helper said nil. Same fix as protocol.lua's globalIs.
|
|
108
|
+
local GLOBAL_GETTERS = {
|
|
109
|
+
io = function() return io end,
|
|
110
|
+
GetMod = function() return GetMod end,
|
|
111
|
+
Game = function() return Game end,
|
|
112
|
+
TweakDB = function() return TweakDB end,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
local function have(name)
|
|
116
|
+
local get = GLOBAL_GETTERS[name]
|
|
117
|
+
if not get then return false, "unlisted" end
|
|
118
|
+
local okG, v = pcall(get)
|
|
119
|
+
if not okG then return false, "nil" end
|
|
120
|
+
local t = type(v)
|
|
121
|
+
return (t == "table" or t == "userdata" or t == "function"), t
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
--- Turn an engine value into something `Protocol.encode` can carry. Engine handles are
|
|
125
|
+
--- `userdata`; `tostring` is the only thing attested to work on one (shipped mods
|
|
126
|
+
--- print `tostring(...)` of engine values), so that is what is recorded.
|
|
127
|
+
local function coerce(v)
|
|
128
|
+
local t = type(v)
|
|
129
|
+
if t == "nil" or t == "string" or t == "number" or t == "boolean" then return v end
|
|
130
|
+
if t == "table" then
|
|
131
|
+
local out = {}
|
|
132
|
+
for k, val in pairs(v) do
|
|
133
|
+
local kt = type(k)
|
|
134
|
+
if kt == "string" or kt == "number" then
|
|
135
|
+
local vt = type(val)
|
|
136
|
+
if vt == "string" or vt == "number" or vt == "boolean" then out[k] = val
|
|
137
|
+
else out[k] = tostring(val) end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
return out
|
|
141
|
+
end
|
|
142
|
+
return tostring(v)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
--- `table.pack` is 5.2+, and this file is kept inside the 5.1-compatible subset the two
|
|
146
|
+
--- shipped CET mods use (README.md "Lua checking"; nothing on disk pins which dialect CET
|
|
147
|
+
--- runs). So pack by hand: `select("#", ...)` counts the values including nils, which
|
|
148
|
+
--- `#{...}` cannot. Returns (count, table).
|
|
149
|
+
local function packResults(...)
|
|
150
|
+
return select("#", ...), { ... }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
--- Read a Vector4-like handle's x/y/z. Shipped CET mods do exactly this
|
|
154
|
+
--- (`pos.x + dx, pos.y + dy, pos.z + dz`), so the field access is attested.
|
|
155
|
+
local function xyz(v)
|
|
156
|
+
if v == nil then return nil end
|
|
157
|
+
local out
|
|
158
|
+
local okR = pcall(function()
|
|
159
|
+
out = { x = tonumber(v.x), y = tonumber(v.y), z = tonumber(v.z) }
|
|
160
|
+
end)
|
|
161
|
+
if not okR or out == nil or out.x == nil then return nil end
|
|
162
|
+
return out
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
--- Read a quaternion handle's i/j/k/r. Shipped CET mods read `q.i, q.j, q.k, q.r`.
|
|
166
|
+
local function quat(q)
|
|
167
|
+
if q == nil then return nil end
|
|
168
|
+
local out
|
|
169
|
+
local okR = pcall(function()
|
|
170
|
+
out = { i = tonumber(q.i), j = tonumber(q.j), k = tonumber(q.k), r = tonumber(q.r) }
|
|
171
|
+
end)
|
|
172
|
+
if not okR or out == nil or out.i == nil then return nil end
|
|
173
|
+
return out
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
--- `Game.GetPlayer()` — wiki/Script-tutorial.md §"Regular handles" ("we can only get the
|
|
177
|
+
--- handle of the `player` by calling the global function `Game.GetPlayer()`");
|
|
178
|
+
--- shipped CET mods call it the same way (`if not Game.GetPlayer() then return end`).
|
|
179
|
+
local function player()
|
|
180
|
+
local p
|
|
181
|
+
local okP = pcall(function() p = Game.GetPlayer() end)
|
|
182
|
+
if okP and p ~= nil then return p end
|
|
183
|
+
return nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
--- `Game.GetDynamicEntitySystem():GetTaggedIDs(tag)` — a shipped CET mod
|
|
187
|
+
--- (`local des = Game.GetDynamicEntitySystem()` … `des:GetTaggedIDs(tag) or {}`).
|
|
188
|
+
local function taggedIds(tag)
|
|
189
|
+
local ids
|
|
190
|
+
local okT, e = pcall(function()
|
|
191
|
+
local des = Game.GetDynamicEntitySystem()
|
|
192
|
+
ids = des:GetTaggedIDs(tag) or {}
|
|
193
|
+
end)
|
|
194
|
+
if not okT then return nil, tostring(e) end
|
|
195
|
+
if type(ids) ~= "table" then return nil, "GetTaggedIDs returned " .. type(ids) end
|
|
196
|
+
return ids, nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
--- `Game.FindEntityByID(id)` — shipped CET mods
|
|
200
|
+
--- (`return Game.FindEntityByID(lightID)`).
|
|
201
|
+
local function entityById(id)
|
|
202
|
+
local ent
|
|
203
|
+
local okE, e = pcall(function() ent = Game.FindEntityByID(id) end)
|
|
204
|
+
if not okE then return nil, tostring(e) end
|
|
205
|
+
return ent, nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
--- `Game.GetTransactionSystem():GetItemInSlot(player, TweakDBID.new(slot))` —
|
|
209
|
+
--- as shipped CET mods do. Returns (itemObject|nil, err).
|
|
210
|
+
local function itemInSlot(slot)
|
|
211
|
+
local p = player()
|
|
212
|
+
if p == nil then return nil, "Game.GetPlayer() returned nil — load a save first" end
|
|
213
|
+
local item
|
|
214
|
+
local okI, e = pcall(function()
|
|
215
|
+
item = Game.GetTransactionSystem():GetItemInSlot(p, TweakDBID.new(slot))
|
|
216
|
+
end)
|
|
217
|
+
if not okI then return nil, tostring(e) end
|
|
218
|
+
return item, nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
--- `ItemID.GetTDBID(w:GetItemData():GetID())` — as shipped CET mods do.
|
|
222
|
+
local function itemTdbid(item)
|
|
223
|
+
local tdbid
|
|
224
|
+
local okT, e = pcall(function() tdbid = ItemID.GetTDBID(item:GetItemData():GetID()) end)
|
|
225
|
+
if not okT then return nil, tostring(e) end
|
|
226
|
+
return tdbid, nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
--- Resolve the `entityRef` parameter shared by the entity rows.
|
|
230
|
+
---
|
|
231
|
+
--- A BARE ENTITY-ID STRING CANNOT BE RESOLVED. `Game.FindEntityByID` is attested
|
|
232
|
+
--- (shipped mods) but only ever with an id handed back by the engine — nothing on disk
|
|
233
|
+
--- attests any call that turns a string back into an EntityID, so asking for one is answered
|
|
234
|
+
--- with an error naming the three forms that DO work rather than a guess:
|
|
235
|
+
--- entityRef = "$player" the player (Game.GetPlayer)
|
|
236
|
+
--- entityRef = "$equipped", params.slot = "…" the item in an equipment slot
|
|
237
|
+
--- params.tag = "…" (+ optional params.index) the nth dynamic entity carrying that tag
|
|
238
|
+
--- Returns (entity, describe, err).
|
|
239
|
+
local function resolveEntity(p)
|
|
240
|
+
local ref = p.entityRef and tostring(p.entityRef) or nil
|
|
241
|
+
|
|
242
|
+
if p.tag ~= nil then
|
|
243
|
+
local tag = tostring(p.tag)
|
|
244
|
+
local ids, e = taggedIds(tag)
|
|
245
|
+
if ids == nil then return nil, nil, "GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e) end
|
|
246
|
+
local index = tonumber(p.index) or 1
|
|
247
|
+
if #ids == 0 then
|
|
248
|
+
return nil, nil, "no dynamic entity carries the tag \"" .. tag .. "\" right now — " ..
|
|
249
|
+
"stream the location in (stand at the marker) and re-run"
|
|
250
|
+
end
|
|
251
|
+
if ids[index] == nil then
|
|
252
|
+
return nil, nil, "params.index " .. tostring(index) .. " is out of range: " .. #ids ..
|
|
253
|
+
" entit" .. ((#ids == 1) and "y" or "ies") .. " carry the tag \"" .. tag .. "\""
|
|
254
|
+
end
|
|
255
|
+
local ent, e2 = entityById(ids[index])
|
|
256
|
+
if ent == nil then
|
|
257
|
+
return nil, nil, "Game.FindEntityByID resolved nothing for tag \"" .. tag .. "\" #" ..
|
|
258
|
+
tostring(index) .. (e2 and (": " .. e2) or " (the entity may still be streaming in)")
|
|
259
|
+
end
|
|
260
|
+
return ent, { via = "tag", tag = tag, index = index, taggedCount = #ids }, nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if ref == "$player" then
|
|
264
|
+
local pl = player()
|
|
265
|
+
if pl == nil then return nil, nil, "Game.GetPlayer() returned nil — load a save first" end
|
|
266
|
+
return pl, { via = "$player" }, nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if ref == "$equipped" then
|
|
270
|
+
local slot = p.slot and tostring(p.slot) or nil
|
|
271
|
+
if slot == nil then return nil, nil, 'entityRef "$equipped" needs params.slot, e.g. "AttachmentSlots.WeaponRight"' end
|
|
272
|
+
local item, e = itemInSlot(slot)
|
|
273
|
+
if item == nil then
|
|
274
|
+
return nil, nil, "nothing is in slot " .. slot .. (e and (": " .. e) or " — equip the item by hand first")
|
|
275
|
+
end
|
|
276
|
+
return item, { via = "$equipped", slot = slot }, nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
return nil, nil,
|
|
280
|
+
"cannot resolve entityRef " .. tostring(ref) .. ": nothing on disk attests a call that turns an entity-id " ..
|
|
281
|
+
'string back into an EntityID. Use params.tag (+ params.index), entityRef "$player", or entityRef ' ..
|
|
282
|
+
'"$equipped" with params.slot.'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
--- `ent:FindComponentByName(name)` — a shipped CET mod
|
|
286
|
+
--- (`return ent:FindComponentByName("SomeGlow")`). Returns (found, err).
|
|
287
|
+
local function componentPresent(ent, name)
|
|
288
|
+
local comp
|
|
289
|
+
local okC, e = pcall(function() comp = ent:FindComponentByName(name) end)
|
|
290
|
+
if not okC then return nil, tostring(e) end
|
|
291
|
+
return comp ~= nil, nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
--- The component sweep shared by `cp2077.entity.components`, `cp2077.interaction.present`
|
|
295
|
+
--- and the appearance differential. Returns (foundArray, mapTable, errArray).
|
|
296
|
+
local function sweepComponents(ent, names)
|
|
297
|
+
local found = Protocol.array({})
|
|
298
|
+
local map = {}
|
|
299
|
+
local errs = Protocol.array({})
|
|
300
|
+
for _, raw in ipairs(names) do
|
|
301
|
+
local name = tostring(raw)
|
|
302
|
+
local present, e = componentPresent(ent, name)
|
|
303
|
+
if present == nil then
|
|
304
|
+
map[name] = "error"
|
|
305
|
+
errs[#errs + 1] = name .. ": " .. tostring(e)
|
|
306
|
+
else
|
|
307
|
+
map[name] = present
|
|
308
|
+
if present then found[#found + 1] = name end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
return found, map, errs
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
-- ---------------------------------------------------------------------------
|
|
315
|
+
-- The dispatch table (in table order)
|
|
316
|
+
-- ---------------------------------------------------------------------------
|
|
317
|
+
-- MODWRIGHT-PROBE-TABLE-BEGIN
|
|
318
|
+
Probes.handlers = {
|
|
319
|
+
|
|
320
|
+
-- Row 1 — **unverified**. "Where can the bridge write?" `io.open(rel, "w")` over the
|
|
321
|
+
-- write-root candidate list; nothing attests CET's `io` for a mod. Confirmed or
|
|
322
|
+
-- contradicted by the discovery result. The discovery itself already ran at onInit; this row
|
|
323
|
+
-- reports it rather than re-running it, so a test plan cannot flap the transport mid-run.
|
|
324
|
+
["cp2077.bridge.io"] = {
|
|
325
|
+
attestation = "unverified",
|
|
326
|
+
citation = "write-root discovery — no source attests CET's io for a mod",
|
|
327
|
+
effect = "read",
|
|
328
|
+
run = function()
|
|
329
|
+
local present, t = have("io")
|
|
330
|
+
local evidence = {
|
|
331
|
+
writeRoot = Protocol.writeRoot,
|
|
332
|
+
writeRootPrefix = Protocol.writeRootPrefix,
|
|
333
|
+
transport = Protocol.transport,
|
|
334
|
+
candidates = Protocol.array({}),
|
|
335
|
+
failures = Protocol.array({}),
|
|
336
|
+
capabilities = {
|
|
337
|
+
io = Protocol.caps.ioOpen == true,
|
|
338
|
+
json = Protocol.caps.json == true,
|
|
339
|
+
TweakDB = Protocol.caps.TweakDB == true,
|
|
340
|
+
Cron = Protocol.caps.Cron == true,
|
|
341
|
+
Observe = Protocol.caps.Observe == true,
|
|
342
|
+
os = Protocol.caps.osTime == true,
|
|
343
|
+
math = Protocol.caps.math == true,
|
|
344
|
+
GetMod = Protocol.caps.GetMod == true,
|
|
345
|
+
},
|
|
346
|
+
requestDecoder = Protocol.decoderName,
|
|
347
|
+
clockSource = Protocol.clockSource,
|
|
348
|
+
moduleLoader = Protocol.loaderUsed,
|
|
349
|
+
typeOfIo = t,
|
|
350
|
+
}
|
|
351
|
+
for i, prefix in ipairs(Protocol.WRITE_ROOT_CANDIDATES) do
|
|
352
|
+
evidence.candidates[i] = prefix .. Protocol.FILES.heartbeat
|
|
353
|
+
end
|
|
354
|
+
for i, e in ipairs(Protocol.writeRootErrors) do evidence.failures[i] = e end
|
|
355
|
+
|
|
356
|
+
if not present then
|
|
357
|
+
return errored("io is absent from this CET sandbox (type(io) = " .. t .. "): the bridge is in " ..
|
|
358
|
+
"log-only mode and every result is printed as a [MODWRIGHT-RESULT] line. This is " ..
|
|
359
|
+
"recorded as `cp2077.bridge.io` CONTRADICTED.", evidence)
|
|
360
|
+
end
|
|
361
|
+
if Protocol.transport == "file" then
|
|
362
|
+
return ok(Protocol.writeRoot, evidence)
|
|
363
|
+
end
|
|
364
|
+
return failed("io exists but every write-root candidate refused a write; the bridge is in log-only mode",
|
|
365
|
+
Protocol.writeRoot, evidence)
|
|
366
|
+
end,
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
-- Row 2 — attested(engine). "The bridge mod is loaded and console-reachable":
|
|
370
|
+
-- `registerForEvent("onInit", …)` and a returned table for `GetMod("ModWrightBridge")` —
|
|
371
|
+
-- knowledge fact cp2077.process.cet-mods-sandboxed (status `verified`, "2.31",
|
|
372
|
+
-- observed in game 2026-08-16, "console access is via GetMod(\"ModName\"), and each mod's init.lua
|
|
373
|
+
-- must return a table"); shipped CET mods follow the same shape.
|
|
374
|
+
["cp2077.mod.loaded"] = {
|
|
375
|
+
attestation = "attested(engine)",
|
|
376
|
+
citation = "knowledge cp2077.process.cet-mods-sandboxed (verified 2.31); shipped CET mods register onInit and return a table",
|
|
377
|
+
effect = "read",
|
|
378
|
+
run = function()
|
|
379
|
+
local hb = Protocol.heartbeat()
|
|
380
|
+
local evidence = { heartbeat = hb }
|
|
381
|
+
local present, t = have("GetMod")
|
|
382
|
+
if not present then
|
|
383
|
+
-- Reaching this handler already proves the mod is loaded; only the CONSOLE
|
|
384
|
+
-- half of the row is unanswerable, and that is said rather than assumed.
|
|
385
|
+
evidence.getModAvailable = false
|
|
386
|
+
evidence.note = "this handler ran, so the mod is loaded; GetMod is " .. t ..
|
|
387
|
+
" here, so console reachability could not be checked from inside the sandbox — " ..
|
|
388
|
+
'run `print(tostring(GetMod("ModWrightBridge")))` in the console to confirm'
|
|
389
|
+
return ok(true, evidence)
|
|
390
|
+
end
|
|
391
|
+
local mod
|
|
392
|
+
local okM, e = pcall(function() mod = GetMod("ModWrightBridge") end)
|
|
393
|
+
evidence.getModAvailable = true
|
|
394
|
+
if not okM then
|
|
395
|
+
evidence.getModError = tostring(e)
|
|
396
|
+
return ok(true, evidence)
|
|
397
|
+
end
|
|
398
|
+
evidence.getModReturned = (mod ~= nil) and type(mod) or "nil"
|
|
399
|
+
evidence.exposes = Protocol.array({})
|
|
400
|
+
if type(mod) == "table" then
|
|
401
|
+
for k in pairs(mod) do evidence.exposes[#evidence.exposes + 1] = tostring(k) end
|
|
402
|
+
table.sort(evidence.exposes)
|
|
403
|
+
end
|
|
404
|
+
if mod == nil then
|
|
405
|
+
return failed('GetMod("ModWrightBridge") returned nil even though this handler is running — ' ..
|
|
406
|
+
"the mod folder name and the GetMod argument must match", false, evidence)
|
|
407
|
+
end
|
|
408
|
+
return ok(true, evidence)
|
|
409
|
+
end,
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
-- Row 3 — attested(engine). "A player exists (i.e. in a save, not the menu)".
|
|
413
|
+
["cp2077.player.present"] = {
|
|
414
|
+
attestation = "attested(engine)",
|
|
415
|
+
citation = "wiki/Script-tutorial.md §\"Regular handles\"; shipped CET mods",
|
|
416
|
+
effect = "read",
|
|
417
|
+
run = function()
|
|
418
|
+
local present, t = have("Game")
|
|
419
|
+
if not present then
|
|
420
|
+
return absent("the `Game` global (type " .. t .. ")", "a console check")
|
|
421
|
+
end
|
|
422
|
+
local p = player()
|
|
423
|
+
local evidence = { handle = p ~= nil and tostring(p) or nil }
|
|
424
|
+
if p == nil then
|
|
425
|
+
return failed("Game.GetPlayer() returned nil — the game is at the menu, or no save is loaded",
|
|
426
|
+
false, evidence)
|
|
427
|
+
end
|
|
428
|
+
return ok(true, evidence)
|
|
429
|
+
end,
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
-- Row 4 — attested(engine). "Which TweakDB item is in a slot".
|
|
433
|
+
-- PRECONDITION: the item is equipped BY HAND, and the console hazard means
|
|
434
|
+
-- the console must be CLOSED while this samples (the weapon drops to the holstered pose
|
|
435
|
+
-- while you type). Give the probe settleSeconds: 2.0.
|
|
436
|
+
["cp2077.item.equipped"] = {
|
|
437
|
+
attestation = "attested(engine)",
|
|
438
|
+
citation = "shipped CET mods (GetItemInSlot, ItemID.GetTDBID)",
|
|
439
|
+
effect = "read",
|
|
440
|
+
run = function(probe)
|
|
441
|
+
local p = params(probe)
|
|
442
|
+
if p.slot == nil then
|
|
443
|
+
return errored('params.slot is required, e.g. "AttachmentSlots.WeaponRight"')
|
|
444
|
+
end
|
|
445
|
+
local slot = tostring(p.slot)
|
|
446
|
+
local evidence = {
|
|
447
|
+
slot = slot,
|
|
448
|
+
precondition = "the item must be equipped by hand and THE CONSOLE MUST BE CLOSED while this samples " ..
|
|
449
|
+
"(the console hazard); use settleSeconds: 2.0",
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
local item, e = itemInSlot(slot)
|
|
453
|
+
if item == nil then
|
|
454
|
+
if e ~= nil then return errored("GetItemInSlot failed: " .. e, evidence) end
|
|
455
|
+
return failed("nothing is in slot " .. slot, nil, evidence)
|
|
456
|
+
end
|
|
457
|
+
local tdbid, e2 = itemTdbid(item)
|
|
458
|
+
if tdbid == nil then
|
|
459
|
+
return errored("ItemID.GetTDBID(item:GetItemData():GetID()) failed: " .. tostring(e2), evidence)
|
|
460
|
+
end
|
|
461
|
+
local observed = tostring(tdbid)
|
|
462
|
+
evidence.tdbid = observed
|
|
463
|
+
|
|
464
|
+
-- `expect` handled here, not by the generic comparator: shipped CET mods
|
|
465
|
+
-- compare TDBIDs as `tdbid == TweakDBID.new(name)`, and the string form of a
|
|
466
|
+
-- TweakDBID is not attested to equal the record path it was built from. So an
|
|
467
|
+
-- `eq`/`ne` against a string is answered with the ATTESTED comparison.
|
|
468
|
+
if type(probe.expect) == "table" and type(probe.expect.value) == "string"
|
|
469
|
+
and (probe.expect.op == "eq" or probe.expect.op == "ne") then
|
|
470
|
+
local want = probe.expect.value
|
|
471
|
+
local same
|
|
472
|
+
local okC, e3 = pcall(function() same = (tdbid == TweakDBID.new(want)) end)
|
|
473
|
+
if not okC then
|
|
474
|
+
return errored("TweakDBID.new(\"" .. want .. "\") comparison failed: " .. tostring(e3), evidence)
|
|
475
|
+
end
|
|
476
|
+
evidence.expect = { op = probe.expect.op, value = want, note = "TweakDBID.new equality (the comparison shipped CET mods use)" }
|
|
477
|
+
local pass = (probe.expect.op == "eq") and same or (not same)
|
|
478
|
+
return {
|
|
479
|
+
status = pass and "pass" or "fail",
|
|
480
|
+
observed = observed,
|
|
481
|
+
evidence = evidence,
|
|
482
|
+
expectHandled = true,
|
|
483
|
+
error = (not pass) and ("slot " .. slot .. " holds " .. observed .. ", not " .. want) or nil,
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
return ok(observed, evidence)
|
|
488
|
+
end,
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
-- Row 5 — attested(doc) FOR THE CALL ONLY. "Give an item (**mutate**)".
|
|
492
|
+
-- `Game.AddToInventory(itemString, quantityString)` — wiki/Console.md §"Specificity"
|
|
493
|
+
-- ('if one wants to call `AddToInventory` they would call `Game.AddToInventory("ItemName", 1)`')
|
|
494
|
+
-- and wiki/ConsoleCommands.md:25, whose body is `WIP`: the RETURN VALUE IS UNDOCUMENTED.
|
|
495
|
+
-- The row's rule: the call's return is recorded but never trusted.
|
|
496
|
+
-- Preconditions: `allowMutate: true` and a disposable save.
|
|
497
|
+
["cp2077.inventory.add"] = {
|
|
498
|
+
attestation = "attested(doc) for the call; success is NOT read from its return",
|
|
499
|
+
citation = "wiki/Console.md §\"Specificity\"; wiki/ConsoleCommands.md:25 (body: WIP)",
|
|
500
|
+
effect = "mutate",
|
|
501
|
+
mutates = true,
|
|
502
|
+
run = function(probe)
|
|
503
|
+
local p = params(probe)
|
|
504
|
+
if p.item == nil then return errored("params.item is required") end
|
|
505
|
+
local item = tostring(p.item)
|
|
506
|
+
-- ConsoleCommands.md:25 names the parameters `itemString, quantityString`, while
|
|
507
|
+
-- Console.md's example passes the number 1. The string form is what the signature
|
|
508
|
+
-- says, so that is what is sent, and both are recorded.
|
|
509
|
+
local qty = tostring(p.qty or p.quantity or 1)
|
|
510
|
+
local evidence = {
|
|
511
|
+
item = item,
|
|
512
|
+
qty = qty,
|
|
513
|
+
signature = "Game.AddToInventory(itemString, quantityString) — ConsoleCommands.md:25",
|
|
514
|
+
returnTrusted = false,
|
|
515
|
+
note = "PASS here means only that the call was invoked without a Lua error. The wiki body for this " ..
|
|
516
|
+
"command is `WIP`, so its return value is undocumented and is recorded verbatim without being " ..
|
|
517
|
+
"interpreted. Pair this row with a following read probe (cp2077.item.equipped, or a TweakDB " ..
|
|
518
|
+
"read) to learn whether anything actually happened.",
|
|
519
|
+
}
|
|
520
|
+
local present = have("Game")
|
|
521
|
+
if not present then return absent("the `Game` global", "a console check") end
|
|
522
|
+
local ret
|
|
523
|
+
local okA, e = pcall(function() ret = Game.AddToInventory(item, qty) end)
|
|
524
|
+
if not okA then
|
|
525
|
+
return errored("Game.AddToInventory(\"" .. item .. "\", \"" .. qty .. "\") raised: " .. tostring(e), evidence)
|
|
526
|
+
end
|
|
527
|
+
evidence.returnValue = coerce(ret)
|
|
528
|
+
evidence.returnType = type(ret)
|
|
529
|
+
return ok(true, evidence)
|
|
530
|
+
end,
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
-- Added 2026-09-11 so that the console lines once pasted by hand
|
|
534
|
+
-- (`Game.AddToInventory(...)`, `TweakDB:GetRecord(...)`, `GetMod(...).where()`) run
|
|
535
|
+
-- through ModWright. Same shape as the BG3 handler: `return <code>` first, the bare chunk
|
|
536
|
+
-- as the fallback, pcall, every return value coerced, the encoded result capped at
|
|
537
|
+
-- `maxBytes` (default 16 KB). Marked `mutates = true` and listed in MUTATING_KINDS: the
|
|
538
|
+
-- bridge cannot know what a chunk does, so it never runs one without allowMutate. The gate
|
|
539
|
+
-- below keys off THIS flag rather than the effect word on the request, so a request that
|
|
540
|
+
-- labelled this kind `read` is skipped exactly like one labelled `mutate`.
|
|
541
|
+
-- Status unverified: nothing here has run in a game, so whether a CET mod's environment
|
|
542
|
+
-- exposes a Lua loader at all is the first thing a launch settles.
|
|
543
|
+
["cp2077.lua.eval"] = {
|
|
544
|
+
attestation = "unverified",
|
|
545
|
+
citation = "added 2026-09-11 so console lines run through ModWright",
|
|
546
|
+
effect = "mutate",
|
|
547
|
+
mutates = true,
|
|
548
|
+
run = function(probe)
|
|
549
|
+
local p = params(probe)
|
|
550
|
+
if type(p.code) ~= "string" or p.code == "" then
|
|
551
|
+
return errored("params.code (a Lua expression or chunk) is required")
|
|
552
|
+
end
|
|
553
|
+
local code = p.code
|
|
554
|
+
-- The catalogue bounds maxBytes at 1..65536 (registry.ts); a request file is not the
|
|
555
|
+
-- catalogue, so an out-of-range value falls back to the default rather than
|
|
556
|
+
-- truncating the answer to nothing.
|
|
557
|
+
local maxBytes = tonumber(p.maxBytes) or 16384
|
|
558
|
+
if maxBytes < 1 or maxBytes > 65536 then maxBytes = 16384 end
|
|
559
|
+
-- `loadstring` first, and this is the half that matters here: this file stays in the
|
|
560
|
+
-- 5.1 subset, and in 5.1 `load` takes a reader FUNCTION, not a string. 5.2+ drops
|
|
561
|
+
-- `loadstring` and `load` takes the string. The third argument ("t": text chunks
|
|
562
|
+
-- only, never a binary chunk) is honoured by 5.2+'s `load`, ignored by 5.1's
|
|
563
|
+
-- `loadstring`.
|
|
564
|
+
local loader = loadstring or load
|
|
565
|
+
if loader == nil then
|
|
566
|
+
return errored("no Lua loader (loadstring/load) is exposed to this mod", { code = code })
|
|
567
|
+
end
|
|
568
|
+
-- Observed on the first launch of this kind (2026-09-11, BG3):
|
|
569
|
+
-- the extender's loader rejected a chunk name as its second argument
|
|
570
|
+
-- ("bad argument #2 to 'loader' (table expected, got string)"), so the
|
|
571
|
+
-- code is the only argument passed; the loader's own defaults apply.
|
|
572
|
+
local fn, compileErr = loader("return " .. code)
|
|
573
|
+
local form = "expression"
|
|
574
|
+
if fn == nil then
|
|
575
|
+
fn, compileErr = loader(code)
|
|
576
|
+
form = "chunk"
|
|
577
|
+
end
|
|
578
|
+
if fn == nil then
|
|
579
|
+
return errored("does not compile: " .. tostring(compileErr), { code = code })
|
|
580
|
+
end
|
|
581
|
+
local n, results = packResults(pcall(fn))
|
|
582
|
+
if not results[1] then
|
|
583
|
+
return errored("raised: " .. tostring(results[2]), { code = code, form = form })
|
|
584
|
+
end
|
|
585
|
+
-- `coerce` maps nil to nil, so a nil among several returned values drops out of this
|
|
586
|
+
-- array instead of holding its place: `returnCount`, not `#observed`, is the
|
|
587
|
+
-- authority on how many values the chunk returned.
|
|
588
|
+
local values = {}
|
|
589
|
+
for i = 2, n do values[#values + 1] = coerce(results[i]) end
|
|
590
|
+
local returnCount = n - 1
|
|
591
|
+
local observed = values[1]
|
|
592
|
+
if returnCount > 1 then observed = values end
|
|
593
|
+
-- Truncation keeps a RAW PREFIX of the encoded JSON, as a plain Lua string. It is
|
|
594
|
+
-- deliberately not valid JSON on its own: the result writer re-encodes `observed`
|
|
595
|
+
-- with Protocol.encode, which escapes it into one JSON string literal, so the result
|
|
596
|
+
-- stays parseable and the reader sees exactly how far the value got.
|
|
597
|
+
local encoded = Protocol.encode(observed)
|
|
598
|
+
local bytes = type(encoded) == "string" and #encoded or nil
|
|
599
|
+
local truncated = false
|
|
600
|
+
if bytes ~= nil and bytes > maxBytes then
|
|
601
|
+
observed = string.sub(encoded, 1, maxBytes)
|
|
602
|
+
truncated = true
|
|
603
|
+
end
|
|
604
|
+
return ok(observed, { code = code, form = form, returnCount = returnCount, bytes = bytes, truncated = truncated })
|
|
605
|
+
end,
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
-- Row 6 — attested(engine). "Find dynamic entities by tag".
|
|
609
|
+
["cp2077.entity.tagged"] = {
|
|
610
|
+
attestation = "attested(engine)",
|
|
611
|
+
citation = "shipped CET mods (GetDynamicEntitySystem/GetTaggedIDs, FindEntityByID)",
|
|
612
|
+
effect = "read",
|
|
613
|
+
run = function(probe)
|
|
614
|
+
local p = params(probe)
|
|
615
|
+
if p.tag == nil then return errored("params.tag is required") end
|
|
616
|
+
local tag = tostring(p.tag)
|
|
617
|
+
local ids, e = taggedIds(tag)
|
|
618
|
+
local evidence = { tag = tag, precondition = "loaded save, player near the entity (it must be streamed in)" }
|
|
619
|
+
if ids == nil then
|
|
620
|
+
return errored("Game.GetDynamicEntitySystem():GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e), evidence)
|
|
621
|
+
end
|
|
622
|
+
local list = Protocol.array({})
|
|
623
|
+
local resolved = 0
|
|
624
|
+
for i, id in ipairs(ids) do
|
|
625
|
+
local ent, e2 = entityById(id)
|
|
626
|
+
if ent ~= nil then resolved = resolved + 1 end
|
|
627
|
+
list[i] = { id = tostring(id), resolved = ent ~= nil, error = e2 }
|
|
628
|
+
end
|
|
629
|
+
evidence.ids = list
|
|
630
|
+
evidence.resolvedCount = resolved
|
|
631
|
+
evidence.taggedCount = #ids
|
|
632
|
+
if #ids == 0 then
|
|
633
|
+
return failed("no dynamic entity carries the tag \"" .. tag .. "\" right now", 0, evidence)
|
|
634
|
+
end
|
|
635
|
+
return ok(#ids, evidence)
|
|
636
|
+
end,
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
-- Row 7 — attested(engine) FOR THE CALL; the component NAMES to ask for are per-case
|
|
640
|
+
-- and **unverified**, which is why `params.names` is required and never defaulted.
|
|
641
|
+
["cp2077.entity.components"] = {
|
|
642
|
+
attestation = "attested(engine) for the call; the component names asked for are unverified (per-case)",
|
|
643
|
+
citation = "shipped CET mods (ent:FindComponentByName)",
|
|
644
|
+
effect = "read",
|
|
645
|
+
run = function(probe)
|
|
646
|
+
local p = params(probe)
|
|
647
|
+
if type(p.names) ~= "table" or #p.names == 0 then
|
|
648
|
+
return errored("params.names must be a non-empty array of component names. There is no default: " ..
|
|
649
|
+
"the names are per-case and unverified, so guessing one would manufacture evidence.")
|
|
650
|
+
end
|
|
651
|
+
local ent, describe, e = resolveEntity(p)
|
|
652
|
+
if ent == nil then return errored(e, { resolve = describe }) end
|
|
653
|
+
|
|
654
|
+
local found, map, errs = sweepComponents(ent, p.names)
|
|
655
|
+
local evidence = {
|
|
656
|
+
resolve = describe,
|
|
657
|
+
asked = Protocol.array(p.names),
|
|
658
|
+
byName = map,
|
|
659
|
+
errors = errs,
|
|
660
|
+
precondition = "the entity must be streamed in",
|
|
661
|
+
}
|
|
662
|
+
if #errs > 0 then
|
|
663
|
+
return errored("FindComponentByName failed for " .. #errs .. " name(s): " .. table.concat(errs, "; "), evidence)
|
|
664
|
+
end
|
|
665
|
+
if #found == 0 then
|
|
666
|
+
return failed("none of the " .. #p.names .. " named components is on this entity", found, evidence)
|
|
667
|
+
end
|
|
668
|
+
return ok(found, evidence)
|
|
669
|
+
end,
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
-- Row 8 — attested(engine) FOR ITEM OBJECTS (shipped mods read these off the drawn
|
|
673
|
+
-- weapon); **unverified** for a sector-placed prop, which a
|
|
674
|
+
-- `GetTaggedIDs`/`GetWorldPosition` console line settles. The attestation reported in the
|
|
675
|
+
-- evidence therefore depends on HOW the entity was resolved.
|
|
676
|
+
["cp2077.entity.transform"] = {
|
|
677
|
+
attestation = "attested(engine) for item objects; unverified for a sector-placed prop",
|
|
678
|
+
citation = "shipped CET mods (GetWorldPosition/GetWorldOrientation on an item object)",
|
|
679
|
+
effect = "read",
|
|
680
|
+
run = function(probe)
|
|
681
|
+
local p = params(probe)
|
|
682
|
+
local ent, describe, e = resolveEntity(p)
|
|
683
|
+
if ent == nil then return errored(e, { resolve = describe }) end
|
|
684
|
+
|
|
685
|
+
local evidence = { resolve = describe, precondition = "the entity must be streamed in" }
|
|
686
|
+
if describe.via == "tag" then
|
|
687
|
+
evidence.attestationForThisCall = "unverified — this is a sector-placed/dynamic prop, not the item " ..
|
|
688
|
+
"object shipped CET mods read; a console line is the confirmation"
|
|
689
|
+
else
|
|
690
|
+
evidence.attestationForThisCall = "attested(engine) — shipped CET mods read these off an item object"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
local pos, orient
|
|
694
|
+
local okP, e1 = pcall(function() pos = ent:GetWorldPosition() end)
|
|
695
|
+
if not okP then return errored("ent:GetWorldPosition() raised: " .. tostring(e1), evidence) end
|
|
696
|
+
local okO, e2 = pcall(function() orient = ent:GetWorldOrientation() end)
|
|
697
|
+
if not okO then evidence.orientationError = tostring(e2) end
|
|
698
|
+
|
|
699
|
+
local position = xyz(pos)
|
|
700
|
+
local orientation = quat(orient)
|
|
701
|
+
evidence.position = position
|
|
702
|
+
evidence.orientation = orientation
|
|
703
|
+
evidence.positionRaw = (position == nil) and tostring(pos) or nil
|
|
704
|
+
if position == nil then
|
|
705
|
+
return failed("GetWorldPosition returned something with no readable x/y/z: " .. tostring(pos), nil, evidence)
|
|
706
|
+
end
|
|
707
|
+
return ok(position, evidence)
|
|
708
|
+
end,
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
-- Row 9 — **unverified**. The acceptance-regression differential: tagged -> components
|
|
712
|
+
-- -> transform, compared against a recorded good baseline.
|
|
713
|
+
--
|
|
714
|
+
-- READ THIS BEFORE TRUSTING THIS ROW.
|
|
715
|
+
-- The observed failure is "a wrong/unresolvable name spawns an INVISIBLE entity"; nothing on disk
|
|
716
|
+
-- attests a visibility predicate in CET Lua, so this probe CANNOT answer "is it visible". It
|
|
717
|
+
-- answers "does this entity carry the same components in the same place as the baseline", and
|
|
718
|
+
-- acceptance pairs it with a manual row ("stand at the marker; is the case drawn?").
|
|
719
|
+
--
|
|
720
|
+
-- With no `params.baseline` it is a CAPTURE, and says so with status `skipped` rather than
|
|
721
|
+
-- reporting a pass it did not earn.
|
|
722
|
+
["cp2077.entity.appearance"] = {
|
|
723
|
+
attestation = "unverified (composite; see the comment above the row)",
|
|
724
|
+
citation = "composed of cp2077.entity.tagged + .components + .transform; see the comment above the row for the failure mode",
|
|
725
|
+
effect = "read",
|
|
726
|
+
run = function(probe)
|
|
727
|
+
local p = params(probe)
|
|
728
|
+
if p.tag == nil then return errored("params.tag is required (the sector node's tag)") end
|
|
729
|
+
if type(p.names) ~= "table" or #p.names == 0 then
|
|
730
|
+
return errored("params.names must be the candidate component-name list to probe; the " ..
|
|
731
|
+
"names are per-case and unverified, so this row never invents one")
|
|
732
|
+
end
|
|
733
|
+
local tag = tostring(p.tag)
|
|
734
|
+
|
|
735
|
+
local ids, e = taggedIds(tag)
|
|
736
|
+
local evidence = {
|
|
737
|
+
tag = tag,
|
|
738
|
+
baselineId = p.baselineId and tostring(p.baselineId) or nil,
|
|
739
|
+
caveat = "this row cannot answer \"is it visible\": no visibility predicate is attested in CET Lua. " ..
|
|
740
|
+
"Pair it with the manual row \"stand at the marker; is the case drawn?\".",
|
|
741
|
+
precondition = "player within streaming range; baseline captured on a known-good build",
|
|
742
|
+
}
|
|
743
|
+
if ids == nil then return errored("GetTaggedIDs(\"" .. tag .. "\") failed: " .. tostring(e), evidence) end
|
|
744
|
+
evidence.taggedCount = #ids
|
|
745
|
+
if #ids == 0 then
|
|
746
|
+
return failed("nothing carries the tag \"" .. tag .. "\" — the entity did not spawn, or is not streamed in",
|
|
747
|
+
nil, evidence)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
local index = tonumber(p.index) or 1
|
|
751
|
+
local ent, e2 = entityById(ids[index])
|
|
752
|
+
if ent == nil then
|
|
753
|
+
return errored("Game.FindEntityByID resolved nothing for \"" .. tag .. "\" #" .. tostring(index) ..
|
|
754
|
+
(e2 and (": " .. e2) or ""), evidence)
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
local found, map = sweepComponents(ent, p.names)
|
|
758
|
+
local pos
|
|
759
|
+
pcall(function() pos = ent:GetWorldPosition() end)
|
|
760
|
+
local capture = {
|
|
761
|
+
components = found,
|
|
762
|
+
byName = map,
|
|
763
|
+
position = xyz(pos),
|
|
764
|
+
taggedCount = #ids,
|
|
765
|
+
}
|
|
766
|
+
evidence.capture = capture
|
|
767
|
+
|
|
768
|
+
local baseline = p.baseline
|
|
769
|
+
if type(baseline) ~= "table" then
|
|
770
|
+
return {
|
|
771
|
+
status = "skipped",
|
|
772
|
+
observed = capture,
|
|
773
|
+
evidence = evidence,
|
|
774
|
+
error = "no params.baseline was supplied, so no differential was computed. This row is a " ..
|
|
775
|
+
"DIFFERENTIAL: record `evidence.capture` from a known-good build as the baseline and re-run " ..
|
|
776
|
+
"with params.baseline set to it.",
|
|
777
|
+
}
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
local baseComponents = {}
|
|
781
|
+
if type(baseline.components) == "table" then
|
|
782
|
+
for _, n in ipairs(baseline.components) do baseComponents[tostring(n)] = true end
|
|
783
|
+
end
|
|
784
|
+
local missing, extra = Protocol.array({}), Protocol.array({})
|
|
785
|
+
for _, n in ipairs(found) do
|
|
786
|
+
if not baseComponents[n] then extra[#extra + 1] = n end
|
|
787
|
+
end
|
|
788
|
+
for n in pairs(baseComponents) do
|
|
789
|
+
if map[n] ~= true then missing[#missing + 1] = n end
|
|
790
|
+
end
|
|
791
|
+
table.sort(missing)
|
|
792
|
+
|
|
793
|
+
local moved = nil
|
|
794
|
+
local basePos = baseline.position
|
|
795
|
+
if type(basePos) == "table" and capture.position ~= nil then
|
|
796
|
+
local dx = (tonumber(basePos.x) or 0) - capture.position.x
|
|
797
|
+
local dy = (tonumber(basePos.y) or 0) - capture.position.y
|
|
798
|
+
local dz = (tonumber(basePos.z) or 0) - capture.position.z
|
|
799
|
+
moved = { dx = dx, dy = dy, dz = dz }
|
|
800
|
+
local tol = tonumber(p.positionTolerance) or 0.05
|
|
801
|
+
moved.withinTolerance = (dx * dx + dy * dy + dz * dz) <= (tol * tol)
|
|
802
|
+
moved.tolerance = tol
|
|
803
|
+
end
|
|
804
|
+
evidence.diff = { missingComponents = missing, extraComponents = extra, position = moved }
|
|
805
|
+
evidence.baseline = { components = Protocol.array(baseline.components or {}), position = basePos }
|
|
806
|
+
|
|
807
|
+
if #missing > 0 or #extra > 0 then
|
|
808
|
+
return failed("the component list differs from the baseline (" .. #missing .. " missing, " .. #extra ..
|
|
809
|
+
" extra); that is the appearance regression's signature when the position is unchanged",
|
|
810
|
+
capture, evidence)
|
|
811
|
+
end
|
|
812
|
+
if moved ~= nil and moved.withinTolerance == false then
|
|
813
|
+
return failed("components match the baseline but the entity moved beyond the tolerance", capture, evidence)
|
|
814
|
+
end
|
|
815
|
+
return ok(capture, evidence)
|
|
816
|
+
end,
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
-- Row 10. `TweakDB:GetRecord(id)`, not attested by any page or mod on disk, but observed
|
|
820
|
+
-- to work from the CET console (2026-09-08):
|
|
821
|
+
-- print(tostring(TweakDB:GetRecord("Items.Preset_Example")))
|
|
822
|
+
["cp2077.tweakdb.record"] = {
|
|
823
|
+
attestation = "attested(engine)",
|
|
824
|
+
citation = "observed in the CET console (2026-09-08): TweakDB:GetRecord(id) returned userdata",
|
|
825
|
+
effect = "read",
|
|
826
|
+
run = function(probe)
|
|
827
|
+
local p = params(probe)
|
|
828
|
+
if p.id == nil then return errored("params.id is required, e.g. \"Items.Preset_Example\"") end
|
|
829
|
+
local id = tostring(p.id)
|
|
830
|
+
local evidence = { id = id, call = "TweakDB:GetRecord(id)" }
|
|
831
|
+
local present, t = have("TweakDB")
|
|
832
|
+
if not present then return missingAttested("`TweakDB` (type " .. t .. ")", "TweakDB:GetRecord", evidence) end
|
|
833
|
+
local rec
|
|
834
|
+
local okR, e = pcall(function() rec = TweakDB:GetRecord(id) end)
|
|
835
|
+
if not okR then
|
|
836
|
+
return errored("TweakDB:GetRecord(\"" .. id .. "\") raised: " .. tostring(e) ..
|
|
837
|
+
" — the call itself is attested (observed 2026-09-08); this is an error for this id or this session, not an unverified call", evidence)
|
|
838
|
+
end
|
|
839
|
+
evidence.returnType = type(rec)
|
|
840
|
+
evidence.returnValue = (rec ~= nil) and tostring(rec) or nil
|
|
841
|
+
if rec == nil then
|
|
842
|
+
return failed("no TweakDB record with id \"" .. id .. "\"", false, evidence)
|
|
843
|
+
end
|
|
844
|
+
return ok(true, evidence)
|
|
845
|
+
end,
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
-- Row 11. `TweakDB:GetFlat(id)`, same standing as row 10.
|
|
849
|
+
-- print(tostring(TweakDB:GetFlat("Items.Preset_Example.displayName")))
|
|
850
|
+
["cp2077.tweakdb.flat"] = {
|
|
851
|
+
attestation = "attested(engine)",
|
|
852
|
+
citation = "observed in the CET console (2026-09-08): TweakDB:GetFlat(id) returned a LocKey",
|
|
853
|
+
effect = "read",
|
|
854
|
+
run = function(probe)
|
|
855
|
+
local p = params(probe)
|
|
856
|
+
if p.id == nil then return errored("params.id is required, e.g. \"Items.Preset_Example.displayName\"") end
|
|
857
|
+
local id = tostring(p.id)
|
|
858
|
+
local evidence = { id = id, call = "TweakDB:GetFlat(id)" }
|
|
859
|
+
local present, t = have("TweakDB")
|
|
860
|
+
if not present then return missingAttested("`TweakDB` (type " .. t .. ")", "TweakDB:GetFlat", evidence) end
|
|
861
|
+
local flat
|
|
862
|
+
local okF, e = pcall(function() flat = TweakDB:GetFlat(id) end)
|
|
863
|
+
if not okF then
|
|
864
|
+
return errored("TweakDB:GetFlat(\"" .. id .. "\") raised: " .. tostring(e) ..
|
|
865
|
+
" — the call itself is attested (observed 2026-09-08); this is an error for this id or this session, not an unverified call", evidence)
|
|
866
|
+
end
|
|
867
|
+
evidence.returnType = type(flat)
|
|
868
|
+
local value = coerce(flat)
|
|
869
|
+
evidence.value = value
|
|
870
|
+
if flat == nil then
|
|
871
|
+
return failed("no TweakDB flat with id \"" .. id .. "\"", nil, evidence)
|
|
872
|
+
end
|
|
873
|
+
return ok(value, evidence)
|
|
874
|
+
end,
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
-- Row 12 — **unverified (name)**. "An interaction component is on the entity" — the
|
|
878
|
+
-- call is `cp2077.entity.components`, but "the *name* is not attested anywhere on disk",
|
|
879
|
+
-- so the name must come from the request and is never guessed at here.
|
|
880
|
+
["cp2077.interaction.present"] = {
|
|
881
|
+
attestation = "unverified (the interaction component's NAME is attested nowhere on disk)",
|
|
882
|
+
citation = "shipped CET mods for the call; the component name itself is unattested",
|
|
883
|
+
effect = "read",
|
|
884
|
+
run = function(probe)
|
|
885
|
+
local p = params(probe)
|
|
886
|
+
local names = p.names
|
|
887
|
+
if type(names) ~= "table" or #names == 0 then
|
|
888
|
+
if p.name ~= nil then names = { p.name } end
|
|
889
|
+
end
|
|
890
|
+
if type(names) ~= "table" or #names == 0 then
|
|
891
|
+
return errored("params.names (or params.name) is required: NO interaction component name is attested " ..
|
|
892
|
+
"by any source on disk, so this row will not guess one. Supply the candidate name(s) you " ..
|
|
893
|
+
"want probed, and record the answer as the confirmation.")
|
|
894
|
+
end
|
|
895
|
+
local ent, describe, e = resolveEntity(p)
|
|
896
|
+
if ent == nil then return errored(e, { resolve = describe }) end
|
|
897
|
+
|
|
898
|
+
local found, map, errs = sweepComponents(ent, names)
|
|
899
|
+
local evidence = {
|
|
900
|
+
resolve = describe,
|
|
901
|
+
asked = Protocol.array(names),
|
|
902
|
+
byName = map,
|
|
903
|
+
errors = errs,
|
|
904
|
+
caveat = "the component NAME is unverified: an absent result may mean the interaction is missing OR " ..
|
|
905
|
+
"that this is the wrong name for it. Only a name confirmed in the field promotes this row.",
|
|
906
|
+
}
|
|
907
|
+
if #errs > 0 then
|
|
908
|
+
return errored("FindComponentByName failed: " .. table.concat(errs, "; "), evidence)
|
|
909
|
+
end
|
|
910
|
+
if #found == 0 then
|
|
911
|
+
return failed("none of the named interaction components is on this entity", found, evidence)
|
|
912
|
+
end
|
|
913
|
+
return ok(found, evidence)
|
|
914
|
+
end,
|
|
915
|
+
},
|
|
916
|
+
|
|
917
|
+
-- Row 13 — **not designed.** "Loot table produced an item": no attested call exists
|
|
918
|
+
-- on disk. It stays a `manual` row until a confirmation step lands
|
|
919
|
+
-- one, so this kind is implemented as a permanent `unsupported`, which is the status
|
|
920
|
+
-- reserved for a kind the bridge cannot answer.
|
|
921
|
+
["cp2077.loot.roll"] = {
|
|
922
|
+
attestation = "not designed",
|
|
923
|
+
citation = "the catalogue's last row — no attested call exists on disk",
|
|
924
|
+
effect = "read",
|
|
925
|
+
run = function()
|
|
926
|
+
return {
|
|
927
|
+
status = "unsupported",
|
|
928
|
+
observed = nil,
|
|
929
|
+
evidence = {
|
|
930
|
+
designStatus = "not designed",
|
|
931
|
+
remedy = "keep this as a `manual` test-plan row (\"kill the enemy; did the item drop?\") until a " ..
|
|
932
|
+
"confirmation step lands an attested call. Nothing in CET on disk reads a loot roll.",
|
|
933
|
+
},
|
|
934
|
+
error = "cp2077.loot.roll is not designed: no attested call exists on disk. It stays a manual row.",
|
|
935
|
+
}
|
|
936
|
+
end,
|
|
937
|
+
},
|
|
938
|
+
}
|
|
939
|
+
-- MODWRIGHT-PROBE-TABLE-END
|
|
940
|
+
|
|
941
|
+
-- ---------------------------------------------------------------------------
|
|
942
|
+
-- Dispatch
|
|
943
|
+
-- ---------------------------------------------------------------------------
|
|
944
|
+
|
|
945
|
+
--- Attach the row's status word and citation, apply `expect`, and stamp the result.
|
|
946
|
+
function Probes.finalize(probe, handler, out)
|
|
947
|
+
local status = out.status or "pass"
|
|
948
|
+
local evidence = out.evidence
|
|
949
|
+
if type(evidence) ~= "table" then evidence = {} end
|
|
950
|
+
if handler ~= nil then
|
|
951
|
+
evidence.attestation = handler.attestation
|
|
952
|
+
evidence.citation = handler.citation
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
if not out.expectHandled and (status == "pass" or status == "fail") and type(probe.expect) == "table" then
|
|
956
|
+
local verdict, note = Probes.evaluateExpect(out.observed, probe.expect)
|
|
957
|
+
if verdict ~= nil then
|
|
958
|
+
status = verdict
|
|
959
|
+
evidence.expect = { op = tostring(probe.expect.op), value = probe.expect.value, note = note }
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
local res = {
|
|
964
|
+
id = tostring(probe.id or "?"),
|
|
965
|
+
kind = tostring(probe.kind or "?"),
|
|
966
|
+
status = status,
|
|
967
|
+
observed = out.observed,
|
|
968
|
+
evidence = evidence,
|
|
969
|
+
ranAt = Protocol.isoNow(),
|
|
970
|
+
}
|
|
971
|
+
if out.error ~= nil then res.error = tostring(out.error) end
|
|
972
|
+
return res
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
local function deepEqual(a, b)
|
|
976
|
+
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
|
977
|
+
return Protocol.encode(a) == Protocol.encode(b)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
local function containsValue(observed, value)
|
|
981
|
+
if observed == nil then return false end
|
|
982
|
+
local needle = tostring(value)
|
|
983
|
+
if type(observed) == "string" then return observed:find(needle, 1, true) ~= nil end
|
|
984
|
+
if type(observed) == "table" then
|
|
985
|
+
for _, v in pairs(observed) do
|
|
986
|
+
if type(v) == "table" then
|
|
987
|
+
if containsValue(v, value) then return true end
|
|
988
|
+
elseif tostring(v) == needle then
|
|
989
|
+
return true
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
return Protocol.encode(observed):find(needle, 1, true) ~= nil
|
|
993
|
+
end
|
|
994
|
+
return tostring(observed) == needle
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
--- The `expect` block: `{ op: "eq"|"ne"|"gte"|"lte"|"contains"|"absent", value }`.
|
|
998
|
+
--- Returns "pass"|"fail"|"error" plus a short note for `evidence.expect`.
|
|
999
|
+
function Probes.evaluateExpect(observed, expect)
|
|
1000
|
+
if type(expect) ~= "table" or expect.op == nil then return nil, nil end
|
|
1001
|
+
local op = tostring(expect.op)
|
|
1002
|
+
local value = expect.value
|
|
1003
|
+
|
|
1004
|
+
if op == "eq" then
|
|
1005
|
+
return deepEqual(observed, value) and "pass" or "fail", "eq"
|
|
1006
|
+
elseif op == "ne" then
|
|
1007
|
+
return (not deepEqual(observed, value)) and "pass" or "fail", "ne"
|
|
1008
|
+
elseif op == "gte" or op == "lte" then
|
|
1009
|
+
local a, b = tonumber(observed), tonumber(value)
|
|
1010
|
+
if a == nil or b == nil then
|
|
1011
|
+
return "error", op .. " needs two numbers; observed=" .. tostring(observed)
|
|
1012
|
+
end
|
|
1013
|
+
if op == "gte" then return (a >= b) and "pass" or "fail", "gte" end
|
|
1014
|
+
return (a <= b) and "pass" or "fail", "lte"
|
|
1015
|
+
elseif op == "contains" then
|
|
1016
|
+
return containsValue(observed, value) and "pass" or "fail", "contains"
|
|
1017
|
+
elseif op == "absent" then
|
|
1018
|
+
local empty = (observed == nil) or (observed == false) or (observed == "")
|
|
1019
|
+
if not empty and type(observed) == "table" then empty = (next(observed) == nil) end
|
|
1020
|
+
return empty and "pass" or "fail", "absent"
|
|
1021
|
+
end
|
|
1022
|
+
return "error", "unknown expect op: " .. op
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
--- The ids allowed to mutate anything: the inventory row it always had, and
|
|
1026
|
+
--- (since 2026-09-11) the eval row. Named
|
|
1027
|
+
--- as a constant so the gate below reads as the rule it enforces.
|
|
1028
|
+
Probes.MUTATING_KINDS = { ["cp2077.inventory.add"] = true, ["cp2077.lua.eval"] = true }
|
|
1029
|
+
|
|
1030
|
+
--- Run one probe. NEVER throws: one probe failing can never abort the batch, and a handler
|
|
1031
|
+
--- that raises is reported as `error` with the raised message.
|
|
1032
|
+
function Probes.run(probe, request, runCtx)
|
|
1033
|
+
local kind = tostring(probe.kind or "")
|
|
1034
|
+
local handler = Probes.handlers[kind]
|
|
1035
|
+
|
|
1036
|
+
if handler == nil then
|
|
1037
|
+
return Probes.finalize(probe, nil, {
|
|
1038
|
+
status = "unsupported",
|
|
1039
|
+
observed = nil,
|
|
1040
|
+
evidence = { kind = kind, context = "cet" },
|
|
1041
|
+
error = "no handler for probe kind \"" .. kind .. "\" in the CET dispatch table (the catalogue lists every id it knows)",
|
|
1042
|
+
})
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
-- The `observe` effect: a listener is armed and the human performs the action.
|
|
1046
|
+
-- No CET listener API is attested by any source on disk (`Observe` is a CET extra that
|
|
1047
|
+
-- nothing here can vouch for), so an observe probe is `unsupported`, not a silent read.
|
|
1048
|
+
if tostring(probe.effect or "read") == "observe" then
|
|
1049
|
+
return Probes.finalize(probe, handler, {
|
|
1050
|
+
status = "unsupported",
|
|
1051
|
+
observed = nil,
|
|
1052
|
+
evidence = { effect = "observe", ObserveAvailable = Protocol.caps.Observe == true },
|
|
1053
|
+
error = "this bridge cannot observe: no CET listener API is attested by any source on disk. Use a " ..
|
|
1054
|
+
"`read` probe with settleSeconds, or a manual test-plan row.",
|
|
1055
|
+
})
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
-- THE MUTATION GATE (keyed on the capability tiers).
|
|
1059
|
+
-- Two conditions, both required: the kind is one of the ids with effect `mutate`, and
|
|
1060
|
+
-- the request was granted the `session` tier (Protocol.grantsTier, which still honours the
|
|
1061
|
+
-- retired `allowMutate: true` alias for one release).
|
|
1062
|
+
local wantsMutation = (handler.mutates == true) or (tostring(probe.effect or "read") == "mutate")
|
|
1063
|
+
if wantsMutation then
|
|
1064
|
+
if Probes.MUTATING_KINDS[kind] ~= true then
|
|
1065
|
+
return Probes.finalize(probe, handler, {
|
|
1066
|
+
status = "skipped",
|
|
1067
|
+
observed = nil,
|
|
1068
|
+
evidence = { effect = "mutate", kind = kind },
|
|
1069
|
+
error = "the probe declares effect \"mutate\" but " .. kind .. " is a read-only row in the catalogue. " ..
|
|
1070
|
+
"The only kinds this bridge lets mutate anything are cp2077.inventory.add and cp2077.lua.eval.",
|
|
1071
|
+
})
|
|
1072
|
+
end
|
|
1073
|
+
if not Protocol.grantsTier(request, "session") then
|
|
1074
|
+
return Probes.finalize(probe, handler, {
|
|
1075
|
+
status = "skipped",
|
|
1076
|
+
observed = nil,
|
|
1077
|
+
evidence = { effect = "mutate", tier = "session", granted = false },
|
|
1078
|
+
error = "effect is \"mutate\" (session tier) and the request was not granted the \"session\" tier. " ..
|
|
1079
|
+
"Grant it via bridge.policy mode: workbench, grant: [\"session\"], or the retired " ..
|
|
1080
|
+
"allowMutate: true alias.",
|
|
1081
|
+
})
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
local okRun, out = pcall(handler.run, probe, request, runCtx)
|
|
1086
|
+
if okRun and type(out) == "table" then return Probes.finalize(probe, handler, out) end
|
|
1087
|
+
Protocol.err("probe:" .. kind, out)
|
|
1088
|
+
return Probes.finalize(probe, handler, {
|
|
1089
|
+
status = "error",
|
|
1090
|
+
observed = nil,
|
|
1091
|
+
evidence = { kind = kind },
|
|
1092
|
+
error = tostring(out),
|
|
1093
|
+
})
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
--- Every kind this bridge answers, sorted — printed by `m.help()` and counted in the banner.
|
|
1097
|
+
function Probes.kinds()
|
|
1098
|
+
local out = {}
|
|
1099
|
+
for k in pairs(Probes.handlers) do out[#out + 1] = k end
|
|
1100
|
+
table.sort(out)
|
|
1101
|
+
return out
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
return Probes
|