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,1038 @@
|
|
|
1
|
+
-- MODWRIGHT-BRIDGE-DO-NOT-SHIP
|
|
2
|
+
--
|
|
3
|
+
-- ModWrightBridge / Probes.lua — the SERVER-context dispatch table. One entry per probe
|
|
4
|
+
-- row of the bridge's probe catalogue, keyed by the row's `id`, and nothing else: probes are
|
|
5
|
+
-- data rather than new Lua precisely because an SE `reset` degrades Osiris, so the probe
|
|
6
|
+
-- list can never be shipped as fresh script.
|
|
7
|
+
--
|
|
8
|
+
-- THE RULE THAT SHAPES EVERY HANDLER HERE (the headline engine finding, observed in game):
|
|
9
|
+
-- in the tested SE build, every Osiris QUERY function returns wrong/false for the entities
|
|
10
|
+
-- probed: Osi.HasPassive, Osi.HasActiveStatus, Osi.IsEnemy, Osi.GetDistanceTo, Osi.IsDead,
|
|
11
|
+
-- Osi.IsPlayer and others are not to be trusted. So every read below is
|
|
12
|
+
-- an ENTITY COMPONENT read, no probe forces a cast, and no Osiris ACTION is ever called.
|
|
13
|
+
--
|
|
14
|
+
-- There are exactly TWO Osiris calls in this file, both deliberate and both named here so a
|
|
15
|
+
-- reviewer never has to hunt for them:
|
|
16
|
+
-- 1. `Osi.GetMaxHitpoints(host)` inside `bg3.osiris.health` — where the query's
|
|
17
|
+
-- unreliability IS the measurement. The degradation detector is
|
|
18
|
+
-- `Osi.GetMaxHitpoints(host)` disagreeing with `Ext.Entity.Get(host).Health.MaxHp`.
|
|
19
|
+
-- 2. `Osi.GetHostCharacter()` to resolve the `"$host"` parameter token used by the
|
|
20
|
+
-- converted test rows. It returns an identity, not a truth value; API.md's own helper
|
|
21
|
+
-- `_C()` is defined as `Ext.Entity.Get(Osi.GetHostCharacter())`, and it is not among
|
|
22
|
+
-- the queries observed to return junk. Every probe that uses it records
|
|
23
|
+
-- `hostResolvedVia` in its evidence so the provenance travels with the answer.
|
|
24
|
+
--
|
|
25
|
+
-- ============================================================================
|
|
26
|
+
-- Ext API used in this file (heading + signature above each first use)
|
|
27
|
+
-- ============================================================================
|
|
28
|
+
-- API.md §"Stats - `Ext.Stats`" / "Ext.Stats.GetStats(type: string): string[]"
|
|
29
|
+
-- API.md §"Stats Objects" / "Ext.Stats.Get(stat, [level], [warnOnError]): StatEntry"
|
|
30
|
+
-- API.md §"Reading stat attributes"
|
|
31
|
+
-- API.md §"Mod info - `Ext.Mod`" / "IsModLoaded(modGuid)", "GetLoadOrder()", "GetMod(modGuid)"
|
|
32
|
+
-- API.md §"Entity class - `Ext.Entity`" / "Entity:GetAllComponentNames() : string[]"
|
|
33
|
+
-- API.md §"Saving the console output to a file" (the attested `Ext.Entity.Get("<uuid>")` call)
|
|
34
|
+
-- API.md §"Utils - `Ext.Utils`" / "Common methods"
|
|
35
|
+
-- (Ext.IO / Ext.Json / Ext.Timer citations live in Protocol.lua, Ext.Osiris in Watch.lua.)
|
|
36
|
+
|
|
37
|
+
local Log = Ext.Require("ModWrightBridge/Log.lua")
|
|
38
|
+
local Protocol = Ext.Require("ModWrightBridge/Protocol.lua")
|
|
39
|
+
local Watch = Ext.Require("ModWrightBridge/Watch.lua")
|
|
40
|
+
|
|
41
|
+
local Probes = {}
|
|
42
|
+
|
|
43
|
+
--- One-shot dump bookkeeping, so the serialized container is dumped on first miss
|
|
44
|
+
--- literally on first miss and not on every poll of a long test plan.
|
|
45
|
+
Probes.dumped = {}
|
|
46
|
+
|
|
47
|
+
--- API.md §"Ext.Stats.GetStats(type: string): string[]" lists thirteen supported types, but the
|
|
48
|
+
--- installed extender (runtime 32, observed in game 2026-09-09) answers four of them
|
|
49
|
+
--- with `bg3se::lua::stats::FetchStatEntries(): Unknown stats entry type: <type>` on the console
|
|
50
|
+
--- (SpellSet, EquipmentSet, ItemGroup, NameGroup) and returns nothing for them, so they are left
|
|
51
|
+
--- out here rather than raising a warning on every stat probe. A stat of one of those types
|
|
52
|
+
--- is not reachable through this probe; say so in the row instead of listing the type.
|
|
53
|
+
local STAT_TYPES = {
|
|
54
|
+
"StatusData", "SpellData", "PassiveData", "Armor", "Weapon", "Character", "Object",
|
|
55
|
+
"TreasureTable", "TreasureCategory",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
local HOST_TOKEN = "$host"
|
|
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 fail(msg, evidence)
|
|
70
|
+
return { status = "fail", observed = nil, evidence = evidence or {}, error = msg }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local function errored(msg, evidence)
|
|
74
|
+
return { status = "error", observed = nil, evidence = evidence or {}, error = msg }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
--- Turn any engine value into something the encoder can carry.
|
|
78
|
+
local function coerce(v)
|
|
79
|
+
local t = type(v)
|
|
80
|
+
if t == "nil" then return nil end
|
|
81
|
+
if t == "string" or t == "number" or t == "boolean" then return v end
|
|
82
|
+
local tbl = Protocol.dumpComponent(v, 4)
|
|
83
|
+
if tbl ~= nil then return tbl end
|
|
84
|
+
return tostring(v)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
--- `table.pack` is 5.2+, and both bridge trees are kept inside the 5.1-compatible subset that
|
|
88
|
+
--- the `luac -p` pass (5.1 and 5.4) and bridges/*/README.md describe. So pack by
|
|
89
|
+
--- hand: `select("#", ...)` counts the values including nils, which `#{...}` cannot.
|
|
90
|
+
--- Returns (count, table).
|
|
91
|
+
local function packResults(...)
|
|
92
|
+
return select("#", ...), { ... }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
--- Resolve a guid parameter, expanding the `"$host"` token. Returns (guid, err, viaOsiris).
|
|
96
|
+
local function resolveGuid(raw)
|
|
97
|
+
if raw == nil then return nil, "params.guid is required" end
|
|
98
|
+
local s = tostring(raw)
|
|
99
|
+
if s ~= HOST_TOKEN then return s, nil, false end
|
|
100
|
+
local ok, host = pcall(function() return Osi.GetHostCharacter() end)
|
|
101
|
+
if ok and host ~= nil and tostring(host) ~= "" then
|
|
102
|
+
return tostring(host), nil, true
|
|
103
|
+
end
|
|
104
|
+
return nil, "could not resolve \"$host\": Osi.GetHostCharacter() returned " .. tostring(host), true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
--- Try the normalised UUID first, then the raw string, because Osiris hands the same
|
|
108
|
+
--- entity in two different forms (a pattern proven in a shipped mod).
|
|
109
|
+
local function getEntity(guid)
|
|
110
|
+
local e
|
|
111
|
+
pcall(function() e = Ext.Entity.Get(Protocol.normGuid(guid)) end)
|
|
112
|
+
if e == nil then pcall(function() e = Ext.Entity.Get(guid) end) end
|
|
113
|
+
return e
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
--- Read one named component off an entity and flatten it. Returns (table, blob, err).
|
|
117
|
+
local function readComponent(guid, componentName)
|
|
118
|
+
local e = getEntity(guid)
|
|
119
|
+
if e == nil then
|
|
120
|
+
return nil, nil, "Ext.Entity.Get returned nil for " .. tostring(guid) .. " (entity not in the current session?)"
|
|
121
|
+
end
|
|
122
|
+
local raw
|
|
123
|
+
local ok = pcall(function() raw = e[componentName] end)
|
|
124
|
+
if not ok or raw == nil then
|
|
125
|
+
return nil, nil, "entity " .. tostring(guid) .. " has no readable ." .. componentName .. " component"
|
|
126
|
+
end
|
|
127
|
+
local tbl, blob = Protocol.dumpComponent(raw, 6)
|
|
128
|
+
if tbl == nil and blob == nil then
|
|
129
|
+
return nil, nil, "." .. componentName .. " could not be flattened by Ext.Json.Stringify{IterateUserdata=true}"
|
|
130
|
+
end
|
|
131
|
+
return tbl, blob, nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
--- Best-effort structural search used by the two rows whose field shape is
|
|
135
|
+
--- NOT pinned. Collects (a) any value stored under a key equal to `needle` and (b) any
|
|
136
|
+
--- table that carries `needle` as one of its own scalar values. Capped, depth-limited, and
|
|
137
|
+
--- always reported alongside the whole dump so the reader can see what was searched.
|
|
138
|
+
local function findNamed(node, needle, out, path, depth)
|
|
139
|
+
if depth > 8 or type(node) ~= "table" or #out >= 16 then return end
|
|
140
|
+
for k, v in pairs(node) do
|
|
141
|
+
local kp = path .. "/" .. tostring(k)
|
|
142
|
+
if tostring(k) == needle then
|
|
143
|
+
out[#out + 1] = { path = kp, value = coerce(v) }
|
|
144
|
+
end
|
|
145
|
+
if type(v) == "table" then
|
|
146
|
+
local names = false
|
|
147
|
+
for _, v2 in pairs(v) do
|
|
148
|
+
if type(v2) ~= "table" and tostring(v2) == needle then names = true end
|
|
149
|
+
end
|
|
150
|
+
if names then out[#out + 1] = { path = kp, value = coerce(v) } end
|
|
151
|
+
findNamed(v, needle, out, kp, depth + 1)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
--- Sample the ActionResources component for one named resource. Returns
|
|
157
|
+
--- (matches|nil, dumpTable, err). `matches` is nil when the resource could not be located —
|
|
158
|
+
--- never a zero that would read as "the resource is empty".
|
|
159
|
+
local function sampleResource(guid, resourceName)
|
|
160
|
+
local tbl, _, err = readComponent(guid, "ActionResources")
|
|
161
|
+
if tbl == nil then return nil, nil, err end
|
|
162
|
+
if resourceName == nil then return nil, tbl, nil end
|
|
163
|
+
local out = {}
|
|
164
|
+
findNamed(tbl, tostring(resourceName), out, "", 1)
|
|
165
|
+
if #out == 0 then return nil, tbl, nil end
|
|
166
|
+
return Protocol.array(out), tbl, nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
--- Status presence via the StatusContainer blob search (search the serialized blob for the
|
|
170
|
+
--- id), the path that actually worked in game once Osi.HasActiveStatus was proven
|
|
171
|
+
--- false-for-present.
|
|
172
|
+
local function statusPresent(guid, statusId)
|
|
173
|
+
local tbl, blob, err = readComponent(guid, "StatusContainer")
|
|
174
|
+
if tbl == nil and blob == nil then return nil, nil, err end
|
|
175
|
+
local hit = false
|
|
176
|
+
if type(blob) == "string" and blob:find(tostring(statusId), 1, true) then hit = true end
|
|
177
|
+
return hit, tbl, nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
-- ---------------------------------------------------------------------------
|
|
181
|
+
-- The dispatch table. Keys are the probe ids, verbatim.
|
|
182
|
+
-- Each entry has `run` (read/mutate), and optionally `arm` + `harvest` (observe).
|
|
183
|
+
-- test/unit/bridge/bg3-lua-parity.test.ts reads the keys back out of this block.
|
|
184
|
+
-- ---------------------------------------------------------------------------
|
|
185
|
+
-- MODWRIGHT-PROBE-TABLE-BEGIN
|
|
186
|
+
Probes.handlers = {
|
|
187
|
+
|
|
188
|
+
-- attested(doc). API.md §"Ext.Stats.Get(stat, [level], [warnOnError]): StatEntry" —
|
|
189
|
+
-- "Returns the specified stats entry as an object for easier manipulation."
|
|
190
|
+
["bg3.stat.exists"] = {
|
|
191
|
+
run = function(probe)
|
|
192
|
+
local p = params(probe)
|
|
193
|
+
if p.name == nil then return errored("params.name is required") end
|
|
194
|
+
local name = tostring(p.name)
|
|
195
|
+
|
|
196
|
+
local entry
|
|
197
|
+
-- warnOnError = false: a deliberate existence check must not spam the SE log.
|
|
198
|
+
pcall(function() entry = Ext.Stats.Get(name, nil, false) end)
|
|
199
|
+
local present = entry ~= nil
|
|
200
|
+
|
|
201
|
+
local searched = Protocol.array({})
|
|
202
|
+
if p.type ~= nil then
|
|
203
|
+
searched[1] = tostring(p.type)
|
|
204
|
+
else
|
|
205
|
+
for i, t in ipairs(STAT_TYPES) do searched[i] = t end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
local foundIn = Protocol.array({})
|
|
209
|
+
for _, t in ipairs(searched) do
|
|
210
|
+
local names
|
|
211
|
+
pcall(function() names = Ext.Stats.GetStats(t) end)
|
|
212
|
+
if type(names) == "table" then
|
|
213
|
+
for _, n in pairs(names) do
|
|
214
|
+
if tostring(n) == name then
|
|
215
|
+
foundIn[#foundIn + 1] = t
|
|
216
|
+
break
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
status = (present or #foundIn > 0) and "pass" or "fail",
|
|
224
|
+
observed = present,
|
|
225
|
+
evidence = { name = name, entryPresent = present, foundInTypes = foundIn, typesSearched = searched },
|
|
226
|
+
}
|
|
227
|
+
end,
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
-- attested(doc). API.md §"Reading stat attributes": "Stat attributes can be retrieved by
|
|
231
|
+
-- reading the appropriate property of the StatEntry object" — `local useCosts = spell.UseCosts`.
|
|
232
|
+
-- "If the stat entry doesn't have the specified attribute ... nil is returned."
|
|
233
|
+
["bg3.stat.fields"] = {
|
|
234
|
+
run = function(probe)
|
|
235
|
+
local p = params(probe)
|
|
236
|
+
if p.name == nil then return errored("params.name is required") end
|
|
237
|
+
if type(p.fields) ~= "table" then return errored("params.fields must be an array of field names") end
|
|
238
|
+
local name = tostring(p.name)
|
|
239
|
+
|
|
240
|
+
local entry
|
|
241
|
+
pcall(function() entry = Ext.Stats.Get(name, nil, false) end)
|
|
242
|
+
if entry == nil then
|
|
243
|
+
return fail("no stats entry named \"" .. name .. "\"", { name = name })
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
local values = {}
|
|
247
|
+
local missing = Protocol.array({})
|
|
248
|
+
local unreadable = Protocol.array({})
|
|
249
|
+
for _, f in ipairs(p.fields) do
|
|
250
|
+
local key = tostring(f)
|
|
251
|
+
local v
|
|
252
|
+
local ok = pcall(function() v = entry[key] end)
|
|
253
|
+
if not ok then
|
|
254
|
+
unreadable[#unreadable + 1] = key
|
|
255
|
+
elseif v == nil then
|
|
256
|
+
missing[#missing + 1] = key
|
|
257
|
+
else
|
|
258
|
+
values[key] = coerce(v)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
status = (#missing == 0 and #unreadable == 0) and "pass" or "fail",
|
|
264
|
+
observed = values,
|
|
265
|
+
evidence = { name = name, values = values, missing = missing, unreadable = unreadable },
|
|
266
|
+
}
|
|
267
|
+
end,
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
-- unverified (testplan/registry.ts). Observed in game (2026-09-10):
|
|
271
|
+
-- Ext.Template.GetTemplate(mapKey) called live at the console returns nil for a loose
|
|
272
|
+
-- Public/<Mod>/RootTemplates/*.lsx, and the full template object (with readable Name/
|
|
273
|
+
-- TemplateType/FileName fields, per a console dump) once the identical
|
|
274
|
+
-- node was converted to a sibling .lsf. That console call is attested; this handler,
|
|
275
|
+
-- reached only through the bridge, has not itself run yet — see knowledge fact
|
|
276
|
+
-- bg3.items.roottemplates.loose-lsx-in-roottemplates-is-not-loaded-a-separate-lsf-is.
|
|
277
|
+
["bg3.template.exists"] = {
|
|
278
|
+
run = function(probe)
|
|
279
|
+
local p = params(probe)
|
|
280
|
+
if p.mapKey == nil then return errored("params.mapKey is required") end
|
|
281
|
+
local mapKey = tostring(p.mapKey)
|
|
282
|
+
|
|
283
|
+
-- Same convention every other handler uses for a required Ext.* call: pcall it,
|
|
284
|
+
-- never let one bad probe take Osiris down with it. A wholly missing `Ext.Template`
|
|
285
|
+
-- global (not expected in a real SE build, but reported like any other missing
|
|
286
|
+
-- precondition rather than guessed past) short-circuits before the call.
|
|
287
|
+
if Ext.Template == nil then return errored("Ext.Template is not available in this build") end
|
|
288
|
+
|
|
289
|
+
-- The pcall's own result is checked here, unlike `bg3.stat.exists`'s, and
|
|
290
|
+
-- deliberately: a `fail` from THIS probe is read as "the template did not reach the
|
|
291
|
+
-- engine", the exact signal the RootTemplates `.lsx` defect produces. A call that
|
|
292
|
+
-- raised instead of returning nil is a different fact entirely, and reporting it as
|
|
293
|
+
-- `fail` would send a reader hunting a packaging bug that is not there. `error` says
|
|
294
|
+
-- "this probe could not answer"; `fail` says "the answer is no".
|
|
295
|
+
local template
|
|
296
|
+
local ok, err = pcall(function() template = Ext.Template.GetTemplate(mapKey) end)
|
|
297
|
+
if not ok then
|
|
298
|
+
return errored("Ext.Template.GetTemplate(\"" .. mapKey .. "\") raised: " .. tostring(err), { mapKey = mapKey })
|
|
299
|
+
end
|
|
300
|
+
if template == nil then
|
|
301
|
+
return { status = "fail", observed = false, evidence = { present = false, mapKey = mapKey } }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
local name, templateType, fileName
|
|
305
|
+
pcall(function() name = coerce(template.Name) end)
|
|
306
|
+
pcall(function() templateType = coerce(template.TemplateType) end)
|
|
307
|
+
pcall(function() fileName = coerce(template.FileName) end)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
status = "pass",
|
|
311
|
+
observed = true,
|
|
312
|
+
evidence = { present = true, mapKey = mapKey, name = name, templateType = templateType, fileName = fileName },
|
|
313
|
+
}
|
|
314
|
+
end,
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
-- attested(doc). API.md §"Mod info - `Ext.Mod`" / "IsModLoaded(modGuid)": "Returns whether
|
|
318
|
+
-- the module with the specified GUID is loaded."; "GetMod(modGuid)" whose `Info: ModuleInfo`
|
|
319
|
+
-- "includes fields such as Name, Author, Description, Directory, ModuleUUID, and ModVersion";
|
|
320
|
+
-- "GetLoadOrder()": "Returns the list of loaded module UUIDs in the order they're loaded in."
|
|
321
|
+
["bg3.mod.active"] = {
|
|
322
|
+
run = function(probe)
|
|
323
|
+
local p = params(probe)
|
|
324
|
+
if p.uuid == nil then return errored("params.uuid is required") end
|
|
325
|
+
local uuid = tostring(p.uuid)
|
|
326
|
+
|
|
327
|
+
local loaded
|
|
328
|
+
pcall(function() loaded = Ext.Mod.IsModLoaded(uuid) end)
|
|
329
|
+
|
|
330
|
+
local modName, modVersion
|
|
331
|
+
pcall(function()
|
|
332
|
+
local m = Ext.Mod.GetMod(uuid)
|
|
333
|
+
if m ~= nil and m.Info ~= nil then
|
|
334
|
+
modName = coerce(m.Info.Name)
|
|
335
|
+
modVersion = coerce(m.Info.ModVersion)
|
|
336
|
+
end
|
|
337
|
+
end)
|
|
338
|
+
|
|
339
|
+
local index
|
|
340
|
+
pcall(function()
|
|
341
|
+
local order = Ext.Mod.GetLoadOrder()
|
|
342
|
+
if type(order) == "table" then
|
|
343
|
+
local i = 0
|
|
344
|
+
for _, u in pairs(order) do
|
|
345
|
+
i = i + 1
|
|
346
|
+
if Protocol.normGuid(tostring(u)) == Protocol.normGuid(uuid) then index = i end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
status = (loaded == true) and "pass" or "fail",
|
|
353
|
+
observed = loaded == true,
|
|
354
|
+
evidence = { uuid = uuid, loaded = loaded == true, modName = modName,
|
|
355
|
+
modVersion = modVersion, loadOrderIndex = index },
|
|
356
|
+
}
|
|
357
|
+
end,
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
-- attested(doc). API.md §"Entity:GetAllComponentNames() : string[]" — "Returns all engine
|
|
361
|
+
-- component types (native C++ class names) that the entity has." Its printed sample is a
|
|
362
|
+
-- name-keyed map, so both shapes are accepted below rather than assuming one.
|
|
363
|
+
["bg3.entity.components"] = {
|
|
364
|
+
run = function(probe)
|
|
365
|
+
local p = params(probe)
|
|
366
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
367
|
+
if guid == nil then return errored(err) end
|
|
368
|
+
|
|
369
|
+
local e = getEntity(guid)
|
|
370
|
+
if e == nil then return fail("Ext.Entity.Get returned nil for " .. guid, { guid = guid }) end
|
|
371
|
+
|
|
372
|
+
local raw
|
|
373
|
+
local ok = pcall(function() raw = e:GetAllComponentNames() end)
|
|
374
|
+
if not ok or type(raw) ~= "table" then
|
|
375
|
+
return errored("Entity:GetAllComponentNames() failed or returned a non-table for " .. guid, { guid = guid })
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
local names = Protocol.array({})
|
|
379
|
+
for k, v in pairs(raw) do
|
|
380
|
+
if type(k) == "string" then names[#names + 1] = k else names[#names + 1] = tostring(v) end
|
|
381
|
+
end
|
|
382
|
+
table.sort(names)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
status = (#names > 0) and "pass" or "fail",
|
|
386
|
+
observed = names,
|
|
387
|
+
evidence = { guid = guid, componentCount = #names, components = names,
|
|
388
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil },
|
|
389
|
+
}
|
|
390
|
+
end,
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
-- attested(engine). Active status -> e.StatusContainer (search the serialized blob for
|
|
394
|
+
-- the id); proven in a shipped mod.
|
|
395
|
+
-- Osi.HasActiveStatus is NOT used: it was observed to return false for a present status.
|
|
396
|
+
-- Precondition worth reading before believing a miss: console `AddBoosts`
|
|
397
|
+
-- do not survive save/load, so test on a fresh save.
|
|
398
|
+
["bg3.entity.status"] = {
|
|
399
|
+
run = function(probe)
|
|
400
|
+
local p = params(probe)
|
|
401
|
+
if p.status == nil then return errored("params.status is required") end
|
|
402
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
403
|
+
if guid == nil then return errored(err) end
|
|
404
|
+
local statusId = tostring(p.status)
|
|
405
|
+
|
|
406
|
+
local hit, tbl, readErr = statusPresent(guid, statusId)
|
|
407
|
+
if hit == nil then return fail(readErr, { guid = guid, status = statusId }) end
|
|
408
|
+
|
|
409
|
+
local evidence = {
|
|
410
|
+
guid = guid, status = statusId, hit = hit,
|
|
411
|
+
readVia = "e.StatusContainer, searched as a serialized blob",
|
|
412
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil,
|
|
413
|
+
}
|
|
414
|
+
-- The serialized container is attached on the first miss only.
|
|
415
|
+
local dumpKey = "status/" .. guid
|
|
416
|
+
if not hit and not Probes.dumped[dumpKey] then
|
|
417
|
+
Probes.dumped[dumpKey] = true
|
|
418
|
+
evidence.statusContainer = tbl
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
return { status = hit and "pass" or "fail", observed = hit, evidence = evidence }
|
|
422
|
+
end,
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
-- attested(engine). `e.PassiveContainer.Passives[].Passive.PassiveId`, a path walked by
|
|
426
|
+
-- a shipped mod.
|
|
427
|
+
["bg3.entity.passive"] = {
|
|
428
|
+
run = function(probe)
|
|
429
|
+
local p = params(probe)
|
|
430
|
+
if p.passive == nil then return errored("params.passive is required") end
|
|
431
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
432
|
+
if guid == nil then return errored(err) end
|
|
433
|
+
local wanted = tostring(p.passive)
|
|
434
|
+
|
|
435
|
+
local tbl, blob, readErr = readComponent(guid, "PassiveContainer")
|
|
436
|
+
if tbl == nil and blob == nil then return fail(readErr, { guid = guid, passive = wanted }) end
|
|
437
|
+
|
|
438
|
+
local ids = Protocol.array({})
|
|
439
|
+
if type(tbl) == "table" and type(tbl.Passives) == "table" then
|
|
440
|
+
for _, row in ipairs(tbl.Passives) do
|
|
441
|
+
local id
|
|
442
|
+
if type(row) == "table" and type(row.Passive) == "table" then id = row.Passive.PassiveId end
|
|
443
|
+
if id ~= nil then ids[#ids + 1] = tostring(id) end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
local hit = false
|
|
448
|
+
for _, id in ipairs(ids) do
|
|
449
|
+
if id == wanted then hit = true end
|
|
450
|
+
end
|
|
451
|
+
local viaBlob = false
|
|
452
|
+
if not hit and type(blob) == "string" and blob:find(wanted, 1, true) then
|
|
453
|
+
hit = true
|
|
454
|
+
viaBlob = true
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
status = hit and "pass" or "fail",
|
|
459
|
+
observed = hit,
|
|
460
|
+
evidence = { guid = guid, passive = wanted, passives = ids, hit = hit,
|
|
461
|
+
matchedViaBlobSearch = viaBlob,
|
|
462
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil },
|
|
463
|
+
}
|
|
464
|
+
end,
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
-- attested(engine). `e.Classes` -> `.Classes[].ClassUUID/.SubClassUUID/.Level`; an
|
|
468
|
+
-- entity probe in a shipped mod proved the shape and field names:
|
|
469
|
+
-- { Classes = { { ClassUUID=..., Level=12, SubClassUUID=... } } }.
|
|
470
|
+
["bg3.entity.class"] = {
|
|
471
|
+
run = function(probe)
|
|
472
|
+
local p = params(probe)
|
|
473
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
474
|
+
if guid == nil then return errored(err) end
|
|
475
|
+
|
|
476
|
+
local tbl, _, readErr = readComponent(guid, "Classes")
|
|
477
|
+
if tbl == nil then return fail(readErr, { guid = guid }) end
|
|
478
|
+
|
|
479
|
+
local rows = Protocol.array({})
|
|
480
|
+
local src = (type(tbl) == "table" and type(tbl.Classes) == "table") and tbl.Classes or nil
|
|
481
|
+
if src ~= nil then
|
|
482
|
+
for _, c in ipairs(src) do
|
|
483
|
+
if type(c) == "table" then
|
|
484
|
+
rows[#rows + 1] = {
|
|
485
|
+
ClassUUID = c.ClassUUID ~= nil and tostring(c.ClassUUID) or nil,
|
|
486
|
+
SubClassUUID = c.SubClassUUID ~= nil and tostring(c.SubClassUUID) or nil,
|
|
487
|
+
Level = tonumber(c.Level),
|
|
488
|
+
}
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
status = (#rows > 0) and "pass" or "fail",
|
|
495
|
+
observed = rows,
|
|
496
|
+
evidence = { guid = guid, classes = rows, component = tbl,
|
|
497
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil },
|
|
498
|
+
}
|
|
499
|
+
end,
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
-- **unverified (shape)**. The component's existence is attested by API.md's
|
|
503
|
+
-- own Entity:GetAllComponentNames() sample output ("eoc::ActionResourcesComponent"), but
|
|
504
|
+
-- the FIELD SHAPE is not documented anywhere. So this probe dumps the whole component
|
|
505
|
+
-- and reports a best-effort structural match, and says so in its evidence. One console
|
|
506
|
+
-- dump of the component on a live character settles it and promotes this row (and
|
|
507
|
+
-- bg3.boost.freecast with it) to attested.
|
|
508
|
+
["bg3.entity.resource"] = {
|
|
509
|
+
run = function(probe)
|
|
510
|
+
local p = params(probe)
|
|
511
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
512
|
+
if guid == nil then return errored(err) end
|
|
513
|
+
|
|
514
|
+
local matches, tbl, readErr = sampleResource(guid, p.resource)
|
|
515
|
+
if tbl == nil then return fail(readErr, { guid = guid, resource = p.resource and tostring(p.resource) or nil }) end
|
|
516
|
+
|
|
517
|
+
local evidence = {
|
|
518
|
+
guid = guid,
|
|
519
|
+
resource = p.resource ~= nil and tostring(p.resource) or nil,
|
|
520
|
+
shapeUnverified = true,
|
|
521
|
+
shapeNote = "bg3.entity.resource is unverified (shape); confirm with a console dump of ActionResources on a live character and pin the SpellSlot path",
|
|
522
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil,
|
|
523
|
+
}
|
|
524
|
+
local dumpKey = "resource/" .. guid
|
|
525
|
+
if not Probes.dumped[dumpKey] then
|
|
526
|
+
Probes.dumped[dumpKey] = true
|
|
527
|
+
evidence.component = tbl
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
if p.resource == nil then
|
|
531
|
+
return { status = "pass", observed = tbl, evidence = evidence }
|
|
532
|
+
end
|
|
533
|
+
if matches == nil then
|
|
534
|
+
evidence.component = tbl
|
|
535
|
+
return errored("could not locate resource \"" .. tostring(p.resource) ..
|
|
536
|
+
"\" inside the ActionResources dump; the field shape is unverified; dump ActionResources at the console to pin it", evidence)
|
|
537
|
+
end
|
|
538
|
+
evidence.matches = matches
|
|
539
|
+
return { status = "pass", observed = matches, evidence = evidence }
|
|
540
|
+
end,
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
-- **unverified** (depends on bg3.entity.resource). The acceptance regression for a
|
|
544
|
+
-- free-cast boost: with the toggle ON, a leveled non-cantrip cast must consume no spell
|
|
545
|
+
-- slot. Sampled before/after ONE player cast inside the window and corroborated by the
|
|
546
|
+
-- toggle status (params.status); the cast is performed by the human (a forced
|
|
547
|
+
-- Osi.UseSpell was observed to re-cast, move the caster and self-kill via the toggle's
|
|
548
|
+
-- HP charge).
|
|
549
|
+
-- Precondition, observed in game: a toggled-off passive's OnCast functors never
|
|
550
|
+
-- fire, so a probe run with the toggle OFF reports `skipped`, never `pass`.
|
|
551
|
+
["bg3.boost.freecast"] = {
|
|
552
|
+
run = function(probe)
|
|
553
|
+
return errored("bg3.boost.freecast is an observe probe: send it with effect \"observe\" and a window.seconds long enough for one cast")
|
|
554
|
+
end,
|
|
555
|
+
arm = function(probe)
|
|
556
|
+
local p = params(probe)
|
|
557
|
+
local guid, err, viaOsiris = resolveGuid(p.guid)
|
|
558
|
+
if guid == nil then return { error = err } end
|
|
559
|
+
local resource = p.resource ~= nil and tostring(p.resource) or "SpellSlot"
|
|
560
|
+
local statusId = p.status ~= nil and tostring(p.status) or "FREECAST_STATUS"
|
|
561
|
+
|
|
562
|
+
local before, dump, readErr = sampleResource(guid, resource)
|
|
563
|
+
local toggled, _, toggleErr = statusPresent(guid, statusId)
|
|
564
|
+
local watch, watchErr = Watch.osiris("CastedSpell", 5, { [1] = guid }, 32)
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
guid = guid, resource = resource, statusId = statusId,
|
|
568
|
+
before = before, beforeDump = dump, readErr = readErr,
|
|
569
|
+
toggled = toggled, toggleErr = toggleErr, watch = watch, watchErr = watchErr,
|
|
570
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil,
|
|
571
|
+
}
|
|
572
|
+
end,
|
|
573
|
+
harvest = function(probe, runCtx, armed)
|
|
574
|
+
if type(armed) ~= "table" or armed.error then
|
|
575
|
+
return errored(tostring(armed and armed.error or "arm state missing"))
|
|
576
|
+
end
|
|
577
|
+
local after, afterDump = sampleResource(armed.guid, armed.resource)
|
|
578
|
+
local casts = (armed.watch ~= nil) and armed.watch.rows or Protocol.array({})
|
|
579
|
+
Watch.stop(armed.watch)
|
|
580
|
+
|
|
581
|
+
local evidence = {
|
|
582
|
+
guid = armed.guid, resource = armed.resource, status = armed.statusId,
|
|
583
|
+
overclockToggledOn = armed.toggled,
|
|
584
|
+
overclockReadError = armed.toggleErr,
|
|
585
|
+
before = armed.before, after = after,
|
|
586
|
+
beforeComponent = armed.beforeDump, afterComponent = afterDump,
|
|
587
|
+
casts = casts, castCount = #casts,
|
|
588
|
+
shapeUnverified = true,
|
|
589
|
+
hostResolvedVia = armed.hostResolvedVia,
|
|
590
|
+
watchError = armed.watchErr,
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if armed.watchErr ~= nil then
|
|
594
|
+
return errored("could not watch CastedSpell: " .. tostring(armed.watchErr), evidence)
|
|
595
|
+
end
|
|
596
|
+
if armed.toggled ~= true then
|
|
597
|
+
local why = "precondition unmet: " .. tostring(armed.statusId) ..
|
|
598
|
+
" is not on the character; a toggled-off passive's OnCast functors never fire."
|
|
599
|
+
if armed.toggleErr ~= nil then
|
|
600
|
+
why = "could not read the toggle status: " .. tostring(armed.toggleErr)
|
|
601
|
+
end
|
|
602
|
+
return { status = "skipped", observed = nil, evidence = evidence, error = why }
|
|
603
|
+
end
|
|
604
|
+
if #casts == 0 then
|
|
605
|
+
return { status = "skipped", observed = nil, evidence = evidence,
|
|
606
|
+
error = "no CastedSpell row for this caster inside the window; the human did not cast (the bridge never forces one)" }
|
|
607
|
+
end
|
|
608
|
+
if armed.before == nil or after == nil then
|
|
609
|
+
return errored("the SpellSlot path inside ActionResources could not be read before and/or after the cast; " ..
|
|
610
|
+
"the component shape is unverified; dump ActionResources at the console to pin it", evidence)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
local unchanged = (Protocol.encode(armed.before) == Protocol.encode(after))
|
|
614
|
+
evidence.unchanged = unchanged
|
|
615
|
+
return {
|
|
616
|
+
status = unchanged and "pass" or "fail",
|
|
617
|
+
observed = unchanged,
|
|
618
|
+
evidence = evidence,
|
|
619
|
+
error = (not unchanged) and "the spell-slot resource changed across the cast: the free-cast boost did not apply" or nil,
|
|
620
|
+
}
|
|
621
|
+
end,
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
-- attested(doc). API.md §"Capturing Events/Calls":
|
|
625
|
+
-- `Ext.Osiris.RegisterListener(name, arity, event, handler)`. Arity must be exact or the
|
|
626
|
+
-- listener is silently dead (observed in game).
|
|
627
|
+
["bg3.event.watch"] = {
|
|
628
|
+
run = function(probe)
|
|
629
|
+
return errored("bg3.event.watch is an observe probe: send it with effect \"observe\" and a window.seconds")
|
|
630
|
+
end,
|
|
631
|
+
arm = function(probe)
|
|
632
|
+
local p = params(probe)
|
|
633
|
+
local match = type(p.match) == "table" and p.match or nil
|
|
634
|
+
if type(match) == "table" then
|
|
635
|
+
-- Expand "$host" inside the match map so a plan row can watch the host without
|
|
636
|
+
-- knowing its GUID.
|
|
637
|
+
for k, v in pairs(match) do
|
|
638
|
+
if tostring(v) == HOST_TOKEN then
|
|
639
|
+
local g = resolveGuid(HOST_TOKEN)
|
|
640
|
+
if g ~= nil then match[k] = g end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
local w, err = Watch.osiris(p.name, p.arity, match, tonumber(p.limit) or 64)
|
|
645
|
+
return { watch = w, error = err, name = p.name, arity = p.arity, match = match }
|
|
646
|
+
end,
|
|
647
|
+
harvest = function(probe, runCtx, armed)
|
|
648
|
+
if type(armed) ~= "table" then return errored("arm state missing") end
|
|
649
|
+
if armed.error ~= nil then return errored(tostring(armed.error), { name = armed.name, arity = armed.arity }) end
|
|
650
|
+
local rows = armed.watch.rows
|
|
651
|
+
Watch.stop(armed.watch)
|
|
652
|
+
return {
|
|
653
|
+
status = (#rows > 0) and "pass" or "fail",
|
|
654
|
+
observed = rows,
|
|
655
|
+
evidence = { name = tostring(armed.name), arity = tonumber(armed.arity),
|
|
656
|
+
match = armed.match, rowCount = #rows, rows = rows,
|
|
657
|
+
timebase = "Ext.Timer.MonotonicTime() milliseconds" },
|
|
658
|
+
}
|
|
659
|
+
end,
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
-- **unverified (event fields)**. `Ext.Events.EsvLuaBeforeDealDamage:Subscribe` is NOT in
|
|
663
|
+
-- API.md's event table — it is attested only by use in a shipped SE mod, and the field
|
|
664
|
+
-- paths inside the event are not pinned. Rows are therefore dumped WHOLE (the first
|
|
665
|
+
-- <=10 raw events) and any `filter` is applied as a substring test over the dump, labelled as such.
|
|
666
|
+
["bg3.event.damage"] = {
|
|
667
|
+
run = function(probe)
|
|
668
|
+
return errored("bg3.event.damage is an observe probe: send it with effect \"observe\" and a window.seconds")
|
|
669
|
+
end,
|
|
670
|
+
arm = function(probe)
|
|
671
|
+
local p = params(probe)
|
|
672
|
+
local w, err = Watch.engineEvent("EsvLuaBeforeDealDamage", tonumber(p.limit) or 10)
|
|
673
|
+
return { watch = w, error = err, filter = type(p.filter) == "table" and p.filter or nil }
|
|
674
|
+
end,
|
|
675
|
+
harvest = function(probe, runCtx, armed)
|
|
676
|
+
if type(armed) ~= "table" then return errored("arm state missing") end
|
|
677
|
+
if armed.error ~= nil then return errored(tostring(armed.error)) end
|
|
678
|
+
local rows = armed.watch.rows
|
|
679
|
+
Watch.stop(armed.watch)
|
|
680
|
+
|
|
681
|
+
local kept = Protocol.array({})
|
|
682
|
+
if armed.filter == nil then
|
|
683
|
+
for i, r in ipairs(rows) do kept[i] = r end
|
|
684
|
+
else
|
|
685
|
+
for _, r in ipairs(rows) do
|
|
686
|
+
local blob = Protocol.encode(r.event)
|
|
687
|
+
local all = true
|
|
688
|
+
for _, want in pairs(armed.filter) do
|
|
689
|
+
if blob:find(tostring(want), 1, true) == nil then all = false end
|
|
690
|
+
end
|
|
691
|
+
if all then kept[#kept + 1] = r end
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
status = (#kept > 0) and "pass" or "fail",
|
|
697
|
+
observed = kept,
|
|
698
|
+
evidence = {
|
|
699
|
+
event = "EsvLuaBeforeDealDamage",
|
|
700
|
+
fieldPathsUnverified = true,
|
|
701
|
+
fieldPathsNote = "the field paths inside this event are not pinned; rows are dumped whole",
|
|
702
|
+
filter = armed.filter,
|
|
703
|
+
filterMode = "plain substring over the whole dumped event, because no field path is pinned",
|
|
704
|
+
captured = #rows, matched = #kept, rows = kept,
|
|
705
|
+
},
|
|
706
|
+
}
|
|
707
|
+
end,
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
-- attested(doc/repo). The degradation detector is `Osi.GetMaxHitpoints(host)` vs
|
|
711
|
+
-- `Ext.Entity.Get(host).Health.MaxHp` disagreeing, after a mid-session `reset` degrades
|
|
712
|
+
-- Osiris (Osi queries return junk). This is the ONE probe
|
|
713
|
+
-- that calls an Osiris query on purpose: the query IS the thing being measured.
|
|
714
|
+
["bg3.osiris.health"] = {
|
|
715
|
+
run = function(probe)
|
|
716
|
+
local p = params(probe)
|
|
717
|
+
local guid, err, viaOsiris = resolveGuid(p.guid ~= nil and p.guid or HOST_TOKEN)
|
|
718
|
+
if guid == nil then return errored(err) end
|
|
719
|
+
|
|
720
|
+
local osiRaw
|
|
721
|
+
pcall(function() osiRaw = Osi.GetMaxHitpoints(guid) end)
|
|
722
|
+
|
|
723
|
+
local ecsRaw
|
|
724
|
+
local e = getEntity(guid)
|
|
725
|
+
if e ~= nil then
|
|
726
|
+
pcall(function() ecsRaw = e.Health.MaxHp end)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
local a, b = tonumber(osiRaw), tonumber(ecsRaw)
|
|
730
|
+
local agree = (a ~= nil and b ~= nil and a == b)
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
status = agree and "pass" or "fail",
|
|
734
|
+
observed = agree,
|
|
735
|
+
evidence = {
|
|
736
|
+
guid = guid,
|
|
737
|
+
osiGetMaxHitpoints = (a ~= nil) and a or tostring(osiRaw),
|
|
738
|
+
entityHealthMaxHp = (b ~= nil) and b or tostring(ecsRaw),
|
|
739
|
+
agree = agree,
|
|
740
|
+
resetCompletedSeenThisState = Protocol.osirisDegraded,
|
|
741
|
+
detector = "Osi.GetMaxHitpoints(host) vs Ext.Entity.Get(host).Health.MaxHp disagreeing",
|
|
742
|
+
hostResolvedVia = viaOsiris and "Osi.GetHostCharacter" or nil,
|
|
743
|
+
},
|
|
744
|
+
error = (not agree) and
|
|
745
|
+
"Osiris disagrees with the ECS read: this Lua state is degraded (a mid-session `reset`). A save reload (F8) is the only clean fix; `reset` reloads Lua only." or nil,
|
|
746
|
+
}
|
|
747
|
+
end,
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
-- attested(doc). API.md §"Utils" / "Common methods": `Ext.Utils.GetGameState()`,
|
|
751
|
+
-- `Ext.Utils.GameVersion(): string?`, `Ext.Utils.Version(): int32`,
|
|
752
|
+
-- `Ext.Utils.GetCommandLineParams(): string[]` (whose documented sample output contains
|
|
753
|
+
-- "--skip-launcher"); the argv is after-the-fact evidence that the game was launched the
|
|
754
|
+
-- confirmed way, since ModWright never launches it. Reports the fixture, never assumes it.
|
|
755
|
+
-- bg3.lua.eval: console lines that used to be pasted by hand
|
|
756
|
+
-- (`_D(Ext.Stats.Get(...))`, `Osi.TemplateAddTo(...)`, `Ext.Mod.GetMod(...).Info.Directory`)
|
|
757
|
+
-- run through ModWright instead. One expression or chunk, compiled with the state's own
|
|
758
|
+
-- `loadstring`/`load` (`return <code>` first, the bare chunk as the fallback), run under
|
|
759
|
+
-- pcall, every return value coerced like any other probe's evidence, and the encoded result
|
|
760
|
+
-- capped at `maxBytes` (default 16 KB) so one careless `Ext.Entity.Get(...)` cannot flood
|
|
761
|
+
-- the result file. Registered with effect "mutate" and never anything weaker: the bridge
|
|
762
|
+
-- cannot know what a chunk does. Protocol's arm gate skips a `mutate` probe unless the
|
|
763
|
+
-- request set allowMutate — but that gate reads the effect word the REQUEST carries, so the
|
|
764
|
+
-- handler refuses outright when that word is anything but "mutate" (below).
|
|
765
|
+
-- Status unverified: nothing here has run in a game, so whether this state exposes a Lua
|
|
766
|
+
-- loader at all is the first thing a launch settles.
|
|
767
|
+
["bg3.lua.eval"] = {
|
|
768
|
+
run = function(probe)
|
|
769
|
+
-- THE GATE, handler side. Protocol.lua's arm phase skips a probe when
|
|
770
|
+
-- `effect == "mutate" and request.allowMutate ~= true`; a request that labelled this
|
|
771
|
+
-- kind "read" (or "observe", which falls through to one sample at harvest time) would
|
|
772
|
+
-- therefore reach this handler with no allowMutate anywhere in the request. The
|
|
773
|
+
-- catalogue gives this kind effect `mutate`, so the handler enforces that rather than
|
|
774
|
+
-- trusting the label on the request file.
|
|
775
|
+
local effect = tostring(probe.effect or "read")
|
|
776
|
+
if effect ~= "mutate" then
|
|
777
|
+
return {
|
|
778
|
+
status = "skipped",
|
|
779
|
+
observed = nil,
|
|
780
|
+
evidence = { effect = effect, kind = "bg3.lua.eval" },
|
|
781
|
+
error = "bg3.lua.eval is a mutate-effect probe: send it with effect \"mutate\" and " ..
|
|
782
|
+
"allowMutate: true. It never runs under a weaker effect, because the bridge cannot " ..
|
|
783
|
+
"know what a chunk does.",
|
|
784
|
+
}
|
|
785
|
+
end
|
|
786
|
+
local p = params(probe)
|
|
787
|
+
if type(p.code) ~= "string" or p.code == "" then
|
|
788
|
+
return errored("params.code (a Lua expression or chunk) is required")
|
|
789
|
+
end
|
|
790
|
+
local code = p.code
|
|
791
|
+
-- The catalogue bounds maxBytes at 1..65536 (registry.ts); a request file is not the
|
|
792
|
+
-- catalogue, so an out-of-range value falls back to the default rather than
|
|
793
|
+
-- truncating the answer to nothing.
|
|
794
|
+
local maxBytes = tonumber(p.maxBytes) or 16384
|
|
795
|
+
if maxBytes < 1 or maxBytes > 65536 then maxBytes = 16384 end
|
|
796
|
+
-- `loadstring` first: wherever it exists the state is 5.1-era, and there `load`
|
|
797
|
+
-- takes a reader FUNCTION, not a string. 5.2+ drops `loadstring` and `load` takes
|
|
798
|
+
-- the string. The third argument ("t": text chunks only, never a binary chunk) is
|
|
799
|
+
-- honoured by 5.2+'s `load` and ignored by 5.1's `loadstring`.
|
|
800
|
+
local loader = loadstring or load
|
|
801
|
+
if loader == nil then
|
|
802
|
+
return errored("no Lua loader (loadstring/load) is exposed in this state", { code = code })
|
|
803
|
+
end
|
|
804
|
+
-- Observed in game (2026-09-11, BG3): the extender's loader rejected a chunk name as its second argument
|
|
805
|
+
-- ("bad argument #2 to 'loader' (table expected, got string)"), so the
|
|
806
|
+
-- code is the only argument passed; the loader's own defaults apply.
|
|
807
|
+
local fn, compileErr = loader("return " .. code)
|
|
808
|
+
local form = "expression"
|
|
809
|
+
if fn == nil then
|
|
810
|
+
fn, compileErr = loader(code)
|
|
811
|
+
form = "chunk"
|
|
812
|
+
end
|
|
813
|
+
if fn == nil then
|
|
814
|
+
return errored("does not compile: " .. tostring(compileErr), { code = code })
|
|
815
|
+
end
|
|
816
|
+
local n, results = packResults(pcall(fn))
|
|
817
|
+
if not results[1] then
|
|
818
|
+
return errored("raised: " .. tostring(results[2]), { code = code, form = form })
|
|
819
|
+
end
|
|
820
|
+
-- `coerce` maps nil to nil, so a nil among several returned values drops out of this
|
|
821
|
+
-- array instead of holding its place: `returnCount`, not `#observed`, is the
|
|
822
|
+
-- authority on how many values the chunk returned.
|
|
823
|
+
local values = {}
|
|
824
|
+
for i = 2, n do values[#values + 1] = coerce(results[i]) end
|
|
825
|
+
local returnCount = n - 1
|
|
826
|
+
local observed = values[1]
|
|
827
|
+
if returnCount > 1 then observed = values end
|
|
828
|
+
-- Truncation keeps a RAW PREFIX of the encoded JSON, as a plain Lua string. It is
|
|
829
|
+
-- deliberately not valid JSON on its own: the result writer re-encodes `observed`
|
|
830
|
+
-- with Protocol.encode, which escapes it into one JSON string literal, so the
|
|
831
|
+
-- result file stays parseable and the reader sees exactly how far the value got.
|
|
832
|
+
local encoded = Protocol.encode(observed)
|
|
833
|
+
local bytes = type(encoded) == "string" and #encoded or nil
|
|
834
|
+
local truncated = false
|
|
835
|
+
if bytes ~= nil and bytes > maxBytes then
|
|
836
|
+
observed = string.sub(encoded, 1, maxBytes)
|
|
837
|
+
truncated = true
|
|
838
|
+
end
|
|
839
|
+
return {
|
|
840
|
+
status = "pass",
|
|
841
|
+
observed = observed,
|
|
842
|
+
evidence = { code = code, form = form, returnCount = returnCount, bytes = bytes, truncated = truncated },
|
|
843
|
+
}
|
|
844
|
+
end,
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
["bg3.precondition.report"] = {
|
|
848
|
+
run = function(probe, runCtx)
|
|
849
|
+
local report = {
|
|
850
|
+
gameState = Protocol.gameState(),
|
|
851
|
+
seVersion = nil,
|
|
852
|
+
gameVersion = nil,
|
|
853
|
+
commandLine = Protocol.commandLine(),
|
|
854
|
+
osirisDegraded = Protocol.osirisDegraded,
|
|
855
|
+
bridgeUptimeMs = math.max(0, Log.monotonic() - Protocol.loadedAtMs),
|
|
856
|
+
}
|
|
857
|
+
local okV, v = pcall(function() return Ext.Utils.Version() end)
|
|
858
|
+
if okV and v ~= nil then report.seVersion = tostring(v) end
|
|
859
|
+
local okG, g = pcall(function() return Ext.Utils.GameVersion() end)
|
|
860
|
+
if okG and g ~= nil then report.gameVersion = tostring(g) end
|
|
861
|
+
|
|
862
|
+
local host, hostErr = resolveGuid(HOST_TOKEN)
|
|
863
|
+
report.host = host
|
|
864
|
+
report.hostError = hostErr
|
|
865
|
+
report.hostResolvedVia = "Osi.GetHostCharacter"
|
|
866
|
+
|
|
867
|
+
if host ~= nil then
|
|
868
|
+
local classProbe = { id = "host-classes", kind = "bg3.entity.class", params = { guid = host } }
|
|
869
|
+
local ok, res = pcall(function() return Probes.handlers["bg3.entity.class"].run(classProbe) end)
|
|
870
|
+
if ok and type(res) == "table" then
|
|
871
|
+
report.hostClasses = res.observed
|
|
872
|
+
report.hostClassStatus = res.status
|
|
873
|
+
else
|
|
874
|
+
report.hostClassError = tostring(res)
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
return { status = "pass", observed = report, evidence = report }
|
|
879
|
+
end,
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
-- attested(engine), RELAYED. The client row; its citation is Ext.IO.SaveFile called from
|
|
883
|
+
-- BootstrapClient.lua, the client-state marker pattern of a shipped SE mod.
|
|
884
|
+
--
|
|
885
|
+
-- The two Lua states cannot see each other (API.md §"Client / Server States": the states
|
|
886
|
+
-- "are completely separated from each other and cannot access the internal state of each
|
|
887
|
+
-- other"), and the protocol has exactly ONE request file and ONE result file per id —
|
|
888
|
+
-- so having both states poll and write the same result would race. The client therefore
|
|
889
|
+
-- writes only its own heartbeat (ModWright_hb_client.json) and this server-side handler
|
|
890
|
+
-- relays it. Every field the row reports is carried there: `stage` is the client's
|
|
891
|
+
-- Ext.Utils.GetGameState(), `seVersion` its bridge.runtimeVersion, `gameVersion` its
|
|
892
|
+
-- bridge.gameVersion. One case this arrangement cannot cover: a client state that never loads
|
|
893
|
+
-- leaves no heartbeat to relay, which the handler reports as a failure.
|
|
894
|
+
["bg3.client.loaded"] = {
|
|
895
|
+
run = function(probe)
|
|
896
|
+
local raw = Protocol.load(Protocol.FILES.heartbeatClient)
|
|
897
|
+
if raw == nil or raw == "" then
|
|
898
|
+
return fail("no " .. Protocol.FILES.heartbeatClient .. " in the Script Extender folder: the client bridge state never loaded",
|
|
899
|
+
{ file = Protocol.FILES.heartbeatClient })
|
|
900
|
+
end
|
|
901
|
+
local hb = Protocol.decode(raw)
|
|
902
|
+
if type(hb) ~= "table" then
|
|
903
|
+
return errored("could not parse " .. Protocol.FILES.heartbeatClient, { file = Protocol.FILES.heartbeatClient })
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
local marker = {
|
|
907
|
+
loaded = true,
|
|
908
|
+
stage = hb.gameState,
|
|
909
|
+
seVersion = (type(hb.bridge) == "table") and hb.bridge.runtimeVersion or nil,
|
|
910
|
+
gameVersion = (type(hb.bridge) == "table") and hb.bridge.gameVersion or nil,
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
-- Freshness, using the same 15 s window as HEARTBEAT_FRESH_MS in
|
|
914
|
+
-- src/core/bridge/locate.ts, so "the client is loaded" never rests on a heartbeat
|
|
915
|
+
-- left behind by an earlier launch.
|
|
916
|
+
local ageSeconds
|
|
917
|
+
local nowSecs = Protocol.epochSeconds()
|
|
918
|
+
local atSecs = Protocol.parseIso(tostring(hb.at or ""))
|
|
919
|
+
if nowSecs ~= nil and atSecs ~= nil then ageSeconds = nowSecs - atSecs end
|
|
920
|
+
|
|
921
|
+
local evidence = {
|
|
922
|
+
file = Protocol.FILES.heartbeatClient,
|
|
923
|
+
marker = marker,
|
|
924
|
+
clientHeartbeatAt = hb.at,
|
|
925
|
+
clientState = hb.state,
|
|
926
|
+
ageSeconds = ageSeconds,
|
|
927
|
+
relayed = true,
|
|
928
|
+
relayNote = "read from the client heartbeat by the server state; the two Lua states are fully separated (API.md §\"Client / Server States\")",
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if ageSeconds ~= nil and ageSeconds > 15 then
|
|
932
|
+
return { status = "fail", observed = marker, evidence = evidence,
|
|
933
|
+
error = ("the client heartbeat is %ds old (>15s): the client bridge state is not running now"):format(ageSeconds) }
|
|
934
|
+
end
|
|
935
|
+
return { status = "pass", observed = marker, evidence = evidence }
|
|
936
|
+
end,
|
|
937
|
+
},
|
|
938
|
+
|
|
939
|
+
}
|
|
940
|
+
-- MODWRIGHT-PROBE-TABLE-END
|
|
941
|
+
|
|
942
|
+
-- ---------------------------------------------------------------------------
|
|
943
|
+
-- Dispatch (Probes.run(probe) looks `kind` up in a fixed table; an unknown kind is
|
|
944
|
+
-- "unsupported"; every handler is pcall-wrapped following the guarded-callback pattern of a
|
|
945
|
+
-- shipped SE mod, whose reason — a bad error path leaves Osiris returning junk — applies
|
|
946
|
+
-- here exactly)
|
|
947
|
+
-- ---------------------------------------------------------------------------
|
|
948
|
+
|
|
949
|
+
--- Wrap a handler's return into a ProbeResult (`probeResultSchema`, .strict()) and apply the
|
|
950
|
+
--- probe's `expect` block.
|
|
951
|
+
function Probes.finalize(probe, out)
|
|
952
|
+
local status = out.status or "pass"
|
|
953
|
+
local evidence = out.evidence
|
|
954
|
+
if type(evidence) ~= "table" then evidence = {} end
|
|
955
|
+
|
|
956
|
+
if (status == "pass" or status == "fail") and type(probe.expect) == "table" then
|
|
957
|
+
local verdict, note = Protocol.evaluateExpect(out.observed, probe.expect)
|
|
958
|
+
if verdict ~= nil then
|
|
959
|
+
status = verdict
|
|
960
|
+
evidence.expect = { op = tostring(probe.expect.op), value = probe.expect.value, note = note }
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
local res = {
|
|
965
|
+
id = tostring(probe.id or "?"),
|
|
966
|
+
kind = tostring(probe.kind or "?"),
|
|
967
|
+
status = status,
|
|
968
|
+
observed = out.observed,
|
|
969
|
+
evidence = evidence,
|
|
970
|
+
ranAt = Protocol.isoNow(),
|
|
971
|
+
}
|
|
972
|
+
if out.error ~= nil then res.error = tostring(out.error) end
|
|
973
|
+
return res
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
--- Run one read/mutate probe. Never throws: one probe failing can never abort the batch.
|
|
977
|
+
function Probes.run(probe, runCtx)
|
|
978
|
+
local kind = tostring(probe.kind or "")
|
|
979
|
+
local h = Probes.handlers[kind]
|
|
980
|
+
if h == nil or h.run == nil then
|
|
981
|
+
return Probes.finalize(probe, {
|
|
982
|
+
status = "unsupported",
|
|
983
|
+
observed = nil,
|
|
984
|
+
evidence = { kind = kind, context = Protocol.context },
|
|
985
|
+
error = "no handler for probe kind \"" .. kind .. "\" in the " .. Protocol.context .. " dispatch table",
|
|
986
|
+
})
|
|
987
|
+
end
|
|
988
|
+
local ok, res = pcall(h.run, probe, runCtx)
|
|
989
|
+
if ok and type(res) == "table" then return Probes.finalize(probe, res) end
|
|
990
|
+
Log.err("probe:" .. kind, res)
|
|
991
|
+
return Probes.finalize(probe, { status = "error", observed = nil, evidence = { kind = kind }, error = tostring(res) })
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
--- Arm one observe probe. Returns the arm state handed back to `harvest`.
|
|
995
|
+
function Probes.arm(probe, runCtx)
|
|
996
|
+
local kind = tostring(probe.kind or "")
|
|
997
|
+
local h = Probes.handlers[kind]
|
|
998
|
+
if h == nil then
|
|
999
|
+
return { unsupported = true, kind = kind }
|
|
1000
|
+
end
|
|
1001
|
+
if h.arm == nil then
|
|
1002
|
+
return { noArm = true, kind = kind }
|
|
1003
|
+
end
|
|
1004
|
+
local ok, state = pcall(h.arm, probe, runCtx)
|
|
1005
|
+
if ok then return state end
|
|
1006
|
+
Log.err("probe-arm:" .. kind, state)
|
|
1007
|
+
return { error = tostring(state), kind = kind }
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
--- Harvest one observe probe at the end of the window.
|
|
1011
|
+
function Probes.harvest(probe, runCtx, armed)
|
|
1012
|
+
local kind = tostring(probe.kind or "")
|
|
1013
|
+
local h = Probes.handlers[kind]
|
|
1014
|
+
|
|
1015
|
+
if type(armed) == "table" and armed.unsupported then
|
|
1016
|
+
return Probes.finalize(probe, {
|
|
1017
|
+
status = "unsupported", observed = nil, evidence = { kind = kind },
|
|
1018
|
+
error = "no handler for probe kind \"" .. kind .. "\" in the " .. Protocol.context .. " dispatch table",
|
|
1019
|
+
})
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
if h ~= nil and h.harvest ~= nil then
|
|
1023
|
+
local ok, res = pcall(h.harvest, probe, runCtx, armed)
|
|
1024
|
+
if ok and type(res) == "table" then return Probes.finalize(probe, res) end
|
|
1025
|
+
Log.err("probe-harvest:" .. kind, res)
|
|
1026
|
+
return Probes.finalize(probe, { status = "error", observed = nil, evidence = { kind = kind }, error = tostring(res) })
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
-- A read-only kind sent with effect "observe": sample it once, at the end of the window,
|
|
1030
|
+
-- and say so rather than pretending a listener watched anything.
|
|
1031
|
+
local res = Probes.run(probe, runCtx)
|
|
1032
|
+
if type(res.evidence) == "table" then
|
|
1033
|
+
res.evidence.observeNote = "this probe kind has no observe handler; it was sampled once at the end of the window"
|
|
1034
|
+
end
|
|
1035
|
+
return res
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
return Probes
|