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,734 @@
|
|
|
1
|
+
import { attr, children, collectAllStatEntries, collectProjectLsx, detectPartialTree, GAME, isProjectTarget, nodesOf, skipFinding, } from "./items.js";
|
|
2
|
+
function listOwners(idx) {
|
|
3
|
+
return { classes: nodesOf(idx, "ClassDescription"), races: nodesOf(idx, "Race") };
|
|
4
|
+
}
|
|
5
|
+
function findOwnerByName(idx, name) {
|
|
6
|
+
const { classes, races } = listOwners(idx);
|
|
7
|
+
const race = races.find((r) => attr(r.node, "Name") === name);
|
|
8
|
+
if (race)
|
|
9
|
+
return { node: race.node, kind: "race" };
|
|
10
|
+
const cls = classes.find((c) => attr(c.node, "Name") === name);
|
|
11
|
+
if (cls)
|
|
12
|
+
return { node: cls.node, kind: attr(cls.node, "ParentGuid") ? "subclass" : "class" };
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
async function isVanillaTableInjection(ctx, name, tableUUID) {
|
|
16
|
+
if (!ctx.store || !name)
|
|
17
|
+
return false;
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await ctx.lookup("guid", { name });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
for (const row of res.rows) {
|
|
26
|
+
const r = row;
|
|
27
|
+
if (r.kind === "ClassDescription" && r.attributes?.ProgressionTableUUID?.value === tableUUID)
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const SEMI = (v) => (v ?? "").split(";").map((s) => s.trim()).filter(Boolean);
|
|
33
|
+
function selectorCalls(selectors, kinds) {
|
|
34
|
+
if (!selectors)
|
|
35
|
+
return [];
|
|
36
|
+
const pattern = new RegExp(`(${kinds.join("|")})\\(([^)]*)\\)`, "g");
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const m of selectors.matchAll(pattern)) {
|
|
39
|
+
out.push({ kind: m[1], args: m[2].split(",").map((a) => a.trim()) });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
const tableuuidMatchesOwner = {
|
|
44
|
+
id: "bg3.progression.tableuuid-matches-owner",
|
|
45
|
+
game: GAME,
|
|
46
|
+
groups: ["progression"],
|
|
47
|
+
modTypes: ["race", "class", "subclass"],
|
|
48
|
+
summary: "A Progression node's TableUUID must equal its owner's ProgressionTableUUID, and differ from its own UUID.",
|
|
49
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
50
|
+
appliesToDescription: "once per project, over every Progression node whose owner is authored in this project",
|
|
51
|
+
needs: { project: true },
|
|
52
|
+
cost: "edit",
|
|
53
|
+
severity: "block",
|
|
54
|
+
evidence: [
|
|
55
|
+
"bg3.races.progression.uuid-vs-tableuuid",
|
|
56
|
+
"bg3.races.progression.race-links-via-progressiontableuuid",
|
|
57
|
+
"bg3.feats.classes.subclass-is-classdescription-with-parentguid",
|
|
58
|
+
],
|
|
59
|
+
async run(ctx) {
|
|
60
|
+
const idx = await collectProjectLsx(ctx);
|
|
61
|
+
const findings = [];
|
|
62
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
63
|
+
const name = attr(ref.node, "Name");
|
|
64
|
+
const tableUUID = attr(ref.node, "TableUUID");
|
|
65
|
+
const uuid = attr(ref.node, "UUID");
|
|
66
|
+
if (!name || !tableUUID)
|
|
67
|
+
continue;
|
|
68
|
+
if (uuid && tableUUID === uuid) {
|
|
69
|
+
findings.push({
|
|
70
|
+
validatorId: this.id,
|
|
71
|
+
severity: "block",
|
|
72
|
+
file: ref.file,
|
|
73
|
+
message: `Progression node "${name}" (UUID=${uuid}) has TableUUID equal to its own UUID — TableUUID must identify the whole table, not this one node.`,
|
|
74
|
+
fix: `Set TableUUID to the value shared by every Progression node of this table (usually the Level 1 node's own value), distinct from this node's UUID.`,
|
|
75
|
+
evidence: this.evidence,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const owner = findOwnerByName(idx, name);
|
|
79
|
+
if (!owner)
|
|
80
|
+
continue;
|
|
81
|
+
const ownerTableUUID = attr(owner.node, "ProgressionTableUUID");
|
|
82
|
+
if (!ownerTableUUID)
|
|
83
|
+
continue;
|
|
84
|
+
if (ownerTableUUID !== tableUUID) {
|
|
85
|
+
findings.push({
|
|
86
|
+
validatorId: this.id,
|
|
87
|
+
severity: "block",
|
|
88
|
+
file: ref.file,
|
|
89
|
+
message: `Progression node "${name}" has TableUUID "${tableUUID}", but its owning ${owner.kind} "${name}" declares ` +
|
|
90
|
+
`ProgressionTableUUID "${ownerTableUUID}" — the two must match or this node's boosts never apply.`,
|
|
91
|
+
fix: `Set TableUUID to "${ownerTableUUID}" (or fix the owner's ProgressionTableUUID, whichever is the typo).`,
|
|
92
|
+
evidence: this.evidence,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return findings;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const level1NodePresent = {
|
|
100
|
+
id: "bg3.progression.level1-node-present",
|
|
101
|
+
game: GAME,
|
|
102
|
+
groups: ["progression"],
|
|
103
|
+
modTypes: ["race", "class", "subclass"],
|
|
104
|
+
summary: "Every ProgressionTableUUID this mod defines nodes for has a Level 1 node (class tables only — " +
|
|
105
|
+
"subclasses legitimately start at whatever level the class allows).",
|
|
106
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
107
|
+
appliesToDescription: "once per project, grouping this mod's own Progression nodes by TableUUID",
|
|
108
|
+
needs: { project: true },
|
|
109
|
+
cost: "edit",
|
|
110
|
+
severity: "block",
|
|
111
|
+
evidence: ["bg3.feats.classes.level1-progression-required-for-cc"],
|
|
112
|
+
async run(ctx) {
|
|
113
|
+
const idx = await collectProjectLsx(ctx);
|
|
114
|
+
const byTable = new Map();
|
|
115
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
116
|
+
const tableUUID = attr(ref.node, "TableUUID");
|
|
117
|
+
if (!tableUUID)
|
|
118
|
+
continue;
|
|
119
|
+
const list = byTable.get(tableUUID);
|
|
120
|
+
if (list)
|
|
121
|
+
list.push(ref);
|
|
122
|
+
else
|
|
123
|
+
byTable.set(tableUUID, [ref]);
|
|
124
|
+
}
|
|
125
|
+
const findings = [];
|
|
126
|
+
for (const [tableUUID, refs] of byTable) {
|
|
127
|
+
const ownerName = attr(refs[0].node, "Name");
|
|
128
|
+
if (await isVanillaTableInjection(ctx, ownerName, tableUUID))
|
|
129
|
+
continue;
|
|
130
|
+
const declaredType = refs.find((r) => attr(r.node, "ProgressionType") != null);
|
|
131
|
+
const progressionType = declaredType ? attr(declaredType.node, "ProgressionType") : undefined;
|
|
132
|
+
if (progressionType !== undefined && progressionType !== "0")
|
|
133
|
+
continue;
|
|
134
|
+
const hasLevel1 = refs.some((r) => Number(attr(r.node, "Level")) === 1);
|
|
135
|
+
if (!hasLevel1) {
|
|
136
|
+
const name = ownerName ?? "?";
|
|
137
|
+
findings.push({
|
|
138
|
+
validatorId: this.id,
|
|
139
|
+
severity: "block",
|
|
140
|
+
file: refs[0].file,
|
|
141
|
+
message: `Table "${tableUUID}" (Progression nodes named "${name}") has no Level 1 node — the class/race cannot progress past character creation.`,
|
|
142
|
+
fix: `Add a Level="1" Progression node with TableUUID="${tableUUID}".`,
|
|
143
|
+
evidence: this.evidence,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return findings;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const OWNER_KIND_TYPE = { class: "0", subclass: "1", race: "2" };
|
|
151
|
+
const typeMatchesOwner = {
|
|
152
|
+
id: "bg3.progression.type-matches-owner",
|
|
153
|
+
game: GAME,
|
|
154
|
+
groups: ["progression"],
|
|
155
|
+
modTypes: ["race", "class", "subclass"],
|
|
156
|
+
summary: "ProgressionType is 0 for a class, 1 for a subclass, 2 for a race, matching the owner.",
|
|
157
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
158
|
+
appliesToDescription: "once per project, over Progression nodes whose owner is authored in this project",
|
|
159
|
+
needs: { project: true },
|
|
160
|
+
cost: "edit",
|
|
161
|
+
severity: "block",
|
|
162
|
+
evidence: [
|
|
163
|
+
"bg3.races.progression.progressiontype-numeric-values",
|
|
164
|
+
"bg3.feats.classes.subclass-listed-under-parent-progression",
|
|
165
|
+
],
|
|
166
|
+
async run(ctx) {
|
|
167
|
+
const idx = await collectProjectLsx(ctx);
|
|
168
|
+
const findings = [];
|
|
169
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
170
|
+
const name = attr(ref.node, "Name");
|
|
171
|
+
const type = attr(ref.node, "ProgressionType");
|
|
172
|
+
if (!name || type === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const owner = findOwnerByName(idx, name);
|
|
175
|
+
if (!owner)
|
|
176
|
+
continue;
|
|
177
|
+
const expected = OWNER_KIND_TYPE[owner.kind];
|
|
178
|
+
if (type !== expected) {
|
|
179
|
+
findings.push({
|
|
180
|
+
validatorId: this.id,
|
|
181
|
+
severity: "block",
|
|
182
|
+
file: ref.file,
|
|
183
|
+
message: `Progression node "${name}" has ProgressionType="${type}", but its owner is a ${owner.kind} (expected ProgressionType="${expected}").`,
|
|
184
|
+
fix: `Set ProgressionType="${expected}".`,
|
|
185
|
+
evidence: this.evidence,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return findings;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const subclassRegisteredInParent = {
|
|
193
|
+
id: "bg3.progression.subclass-registered-in-parent",
|
|
194
|
+
game: GAME,
|
|
195
|
+
groups: ["progression"],
|
|
196
|
+
modTypes: ["subclass"],
|
|
197
|
+
summary: "A subclass ClassDescription must appear as a SubClasses/SubClass child on its parent's Progression node.",
|
|
198
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
199
|
+
appliesToDescription: "once per project, over subclass ClassDescription nodes whose parent class is also authored in this project",
|
|
200
|
+
needs: { project: true },
|
|
201
|
+
cost: "edit",
|
|
202
|
+
severity: "block",
|
|
203
|
+
evidence: ["bg3.feats.classes.subclass-listed-under-parent-progression"],
|
|
204
|
+
async run(ctx) {
|
|
205
|
+
const idx = await collectProjectLsx(ctx);
|
|
206
|
+
const classes = nodesOf(idx, "ClassDescription");
|
|
207
|
+
const progressions = nodesOf(idx, "Progression");
|
|
208
|
+
const findings = [];
|
|
209
|
+
for (const sub of classes) {
|
|
210
|
+
const parentGuid = attr(sub.node, "ParentGuid");
|
|
211
|
+
const subUuid = attr(sub.node, "UUID");
|
|
212
|
+
const subName = attr(sub.node, "Name") ?? "?";
|
|
213
|
+
if (!parentGuid || !subUuid)
|
|
214
|
+
continue;
|
|
215
|
+
const parent = classes.find((c) => attr(c.node, "UUID") === parentGuid);
|
|
216
|
+
if (!parent) {
|
|
217
|
+
findings.push({
|
|
218
|
+
validatorId: this.id,
|
|
219
|
+
severity: "info",
|
|
220
|
+
file: sub.file,
|
|
221
|
+
message: `Subclass "${subName}"'s ParentGuid "${parentGuid}" is not a ClassDescription authored in this project — ` +
|
|
222
|
+
`its registration as a SubClasses/SubClass entry must exist in the parent mod, or be added via the ` +
|
|
223
|
+
`Compatibility Framework.`,
|
|
224
|
+
evidence: this.evidence,
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const parentName = attr(parent.node, "Name");
|
|
229
|
+
const registered = progressions.some((p) => attr(p.node, "Name") === parentName &&
|
|
230
|
+
children(p.node, "SubClasses").some((sc) => children(sc, "SubClass").some((s) => attr(s, "Object") === subUuid)));
|
|
231
|
+
if (!registered) {
|
|
232
|
+
findings.push({
|
|
233
|
+
validatorId: this.id,
|
|
234
|
+
severity: "block",
|
|
235
|
+
file: sub.file,
|
|
236
|
+
message: `Subclass "${subName}" (UUID=${subUuid}) is not registered as a SubClasses/SubClass entry on any of "${parentName}"'s Progression nodes.`,
|
|
237
|
+
fix: `Add a <node id="SubClasses"><children><node id="SubClass"><attribute id="Object" type="guid" value="${subUuid}"/></node></children></node> to the Progression node for the level this subclass unlocks.`,
|
|
238
|
+
related: [parent.file],
|
|
239
|
+
evidence: this.evidence,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return findings;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const INDEX_BACKED_SELECTOR_KINDS = ["SelectSpells", "AddSpells"];
|
|
247
|
+
const spelllistSelectorResolves = {
|
|
248
|
+
id: "bg3.progression.spelllist-selector-resolves",
|
|
249
|
+
game: GAME,
|
|
250
|
+
groups: ["progression"],
|
|
251
|
+
modTypes: ["race", "class", "subclass", "feat"],
|
|
252
|
+
summary: "The first argument of SelectSpells/AddSpells must resolve to a SpellList UUID (see below for SelectPassives/SelectSkills).",
|
|
253
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
254
|
+
appliesToDescription: "once per project, over every SelectSpells/AddSpells call in a Progression node",
|
|
255
|
+
needs: { project: true, index: true },
|
|
256
|
+
cost: "edit",
|
|
257
|
+
severity: "block",
|
|
258
|
+
evidence: [
|
|
259
|
+
"bg3.crash.dangling-spelllist-reference",
|
|
260
|
+
"bg3.practice.reference-checker-invaluable",
|
|
261
|
+
"bg3.spells.addspells-autogrant-pattern",
|
|
262
|
+
],
|
|
263
|
+
async run(ctx) {
|
|
264
|
+
const idx = await collectProjectLsx(ctx);
|
|
265
|
+
const projectLists = new Set(nodesOf(idx, "SpellList")
|
|
266
|
+
.map((ref) => attr(ref.node, "UUID"))
|
|
267
|
+
.filter((v) => !!v));
|
|
268
|
+
const findings = [];
|
|
269
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
270
|
+
const selectors = attr(ref.node, "Selectors");
|
|
271
|
+
for (const call of selectorCalls(selectors, INDEX_BACKED_SELECTOR_KINDS)) {
|
|
272
|
+
const uuid = call.args[0];
|
|
273
|
+
if (!uuid || !/^[0-9a-fA-F-]{36}$/.test(uuid))
|
|
274
|
+
continue;
|
|
275
|
+
if (projectLists.has(uuid))
|
|
276
|
+
continue;
|
|
277
|
+
let resolved = false;
|
|
278
|
+
if (ctx.store) {
|
|
279
|
+
const res = await ctx.lookup("list", { uuid });
|
|
280
|
+
resolved = res.count > 0;
|
|
281
|
+
}
|
|
282
|
+
if (!resolved) {
|
|
283
|
+
findings.push({
|
|
284
|
+
validatorId: this.id,
|
|
285
|
+
severity: "block",
|
|
286
|
+
file: ref.file,
|
|
287
|
+
message: `Progression "${attr(ref.node, "Name") ?? "?"}"'s ${call.kind}(${uuid},...) does not resolve to any list UUID in this mod or the vanilla index.`,
|
|
288
|
+
fix: `Define a SpellList with UUID="${uuid}", or fix the selector argument.`,
|
|
289
|
+
evidence: this.evidence,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return findings;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
const spelllistMembersExist = {
|
|
298
|
+
id: "bg3.progression.spelllist-members-exist",
|
|
299
|
+
game: GAME,
|
|
300
|
+
groups: ["progression"],
|
|
301
|
+
modTypes: ["race", "class", "subclass", "feat"],
|
|
302
|
+
summary: "Every spell named in a SpellList this mod defines resolves to a Spells.txt entry.",
|
|
303
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
304
|
+
appliesToDescription: "once per project, over every SpellList's Spells this mod defines — refuses to run on a partial tree",
|
|
305
|
+
needs: { project: true, index: true },
|
|
306
|
+
cost: "edit",
|
|
307
|
+
severity: "block",
|
|
308
|
+
evidence: ["bg3.crash.dangling-spelllist-reference", "bg3.practice.reference-checker-invaluable"],
|
|
309
|
+
async run(ctx) {
|
|
310
|
+
const partial = await detectPartialTree(ctx);
|
|
311
|
+
if (partial)
|
|
312
|
+
return skipFinding(this.id, ctx.project, partial, this.evidence);
|
|
313
|
+
const idx = await collectProjectLsx(ctx);
|
|
314
|
+
const byName = await collectAllStatEntries(ctx);
|
|
315
|
+
const findings = [];
|
|
316
|
+
for (const ref of nodesOf(idx, "SpellList")) {
|
|
317
|
+
const listName = attr(ref.node, "Name") ?? attr(ref.node, "UUID") ?? "?";
|
|
318
|
+
for (const spell of SEMI(attr(ref.node, "Spells"))) {
|
|
319
|
+
const own = byName.get(spell);
|
|
320
|
+
let resolved = own !== undefined;
|
|
321
|
+
if (!resolved && ctx.store) {
|
|
322
|
+
const res = await ctx.lookup("stat", { name: spell });
|
|
323
|
+
resolved = res.count > 0;
|
|
324
|
+
}
|
|
325
|
+
if (!resolved) {
|
|
326
|
+
findings.push({
|
|
327
|
+
validatorId: this.id,
|
|
328
|
+
severity: "block",
|
|
329
|
+
file: ref.file,
|
|
330
|
+
message: `SpellList "${listName}" names spell "${spell}", which is not a defined stats entry in this mod or vanilla.`,
|
|
331
|
+
fix: `Define "${spell}" as a SpellData entry, or remove it from the list.`,
|
|
332
|
+
evidence: this.evidence,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return findings;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
const passivesExist = {
|
|
341
|
+
id: "bg3.progression.passives-exist",
|
|
342
|
+
game: GAME,
|
|
343
|
+
groups: ["progression"],
|
|
344
|
+
modTypes: ["race", "class", "subclass", "feat"],
|
|
345
|
+
summary: "Every name in PassivesAdded/PassivesRemoved resolves to a Passive.txt entry.",
|
|
346
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
347
|
+
appliesToDescription: "once per project, over every Progression node's PassivesAdded/PassivesRemoved — refuses to run on a partial tree",
|
|
348
|
+
needs: { project: true, index: true },
|
|
349
|
+
cost: "edit",
|
|
350
|
+
severity: "block",
|
|
351
|
+
evidence: [
|
|
352
|
+
"bg3.feats.classes.boosts-vs-passivesadded-distinction",
|
|
353
|
+
"bg3.feats.classes.passivesadded-vs-selectpassives-selector",
|
|
354
|
+
"bg3.practice.reference-checker-invaluable",
|
|
355
|
+
],
|
|
356
|
+
async run(ctx) {
|
|
357
|
+
const partial = await detectPartialTree(ctx);
|
|
358
|
+
if (partial)
|
|
359
|
+
return skipFinding(this.id, ctx.project, partial, this.evidence);
|
|
360
|
+
const idx = await collectProjectLsx(ctx);
|
|
361
|
+
const byName = await collectAllStatEntries(ctx);
|
|
362
|
+
const findings = [];
|
|
363
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
364
|
+
const name = attr(ref.node, "Name") ?? "?";
|
|
365
|
+
for (const field of ["PassivesAdded", "PassivesRemoved"]) {
|
|
366
|
+
for (const passive of SEMI(attr(ref.node, field))) {
|
|
367
|
+
const own = byName.get(passive);
|
|
368
|
+
let resolved = own !== undefined;
|
|
369
|
+
if (!resolved && ctx.store) {
|
|
370
|
+
const res = await ctx.lookup("stat", { name: passive });
|
|
371
|
+
resolved = res.count > 0;
|
|
372
|
+
}
|
|
373
|
+
if (!resolved) {
|
|
374
|
+
findings.push({
|
|
375
|
+
validatorId: this.id,
|
|
376
|
+
severity: "block",
|
|
377
|
+
file: ref.file,
|
|
378
|
+
message: `Progression "${name}"'s ${field} names "${passive}", which is not a defined stats entry in this mod or vanilla.`,
|
|
379
|
+
fix: `Define "${passive}" as a PassiveData entry, or remove it from ${field}.`,
|
|
380
|
+
evidence: this.evidence,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return findings;
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
const addspellsArgSlots = {
|
|
390
|
+
id: "bg3.progression.addspells-arg-slots",
|
|
391
|
+
game: GAME,
|
|
392
|
+
groups: ["progression"],
|
|
393
|
+
modTypes: ["race", "class", "subclass", "feat"],
|
|
394
|
+
summary: "AddSpells argument positions are respected: AlwaysPrepared in slot 5, UntilRest in slot 6.",
|
|
395
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
396
|
+
appliesToDescription: "once per project, over every AddSpells(...) call in a Progression node's Selectors",
|
|
397
|
+
needs: { project: true },
|
|
398
|
+
cost: "edit",
|
|
399
|
+
severity: "warn",
|
|
400
|
+
evidence: ["bg3.spells.untilrest-free-cast-pattern", "bg3.spells.addspells-autogrant-pattern"],
|
|
401
|
+
async run(ctx) {
|
|
402
|
+
const idx = await collectProjectLsx(ctx);
|
|
403
|
+
const findings = [];
|
|
404
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
405
|
+
for (const call of selectorCalls(attr(ref.node, "Selectors"), ["AddSpells"])) {
|
|
406
|
+
const args = call.args;
|
|
407
|
+
const alwaysPreparedAt = args.findIndex((a) => a === "AlwaysPrepared");
|
|
408
|
+
if (alwaysPreparedAt !== -1 && alwaysPreparedAt !== 4) {
|
|
409
|
+
findings.push({
|
|
410
|
+
validatorId: this.id,
|
|
411
|
+
severity: "warn",
|
|
412
|
+
file: ref.file,
|
|
413
|
+
message: `AddSpells(${args.join(",")}) has "AlwaysPrepared" in argument slot ${alwaysPreparedAt + 1}, but it must be slot 5 (prepareType).`,
|
|
414
|
+
fix: `AddSpells(list, selectorId, ability, cooldown, prepareType, replenishType) — move AlwaysPrepared to slot 5.`,
|
|
415
|
+
evidence: this.evidence,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const untilRestAt = args.findIndex((a) => a === "UntilRest");
|
|
419
|
+
if (untilRestAt !== -1 && untilRestAt !== 5) {
|
|
420
|
+
findings.push({
|
|
421
|
+
validatorId: this.id,
|
|
422
|
+
severity: "warn",
|
|
423
|
+
file: ref.file,
|
|
424
|
+
message: `AddSpells(${args.join(",")}) has "UntilRest" in argument slot ${untilRestAt + 1}, but it must be slot 6 (replenishType).`,
|
|
425
|
+
fix: `AddSpells(list, selectorId, ability, cooldown, prepareType, replenishType) — move UntilRest to slot 6.`,
|
|
426
|
+
evidence: this.evidence,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return findings;
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
const classAnchorDescription = {
|
|
435
|
+
id: "bg3.progression.class-anchor-description",
|
|
436
|
+
game: GAME,
|
|
437
|
+
groups: ["progression"],
|
|
438
|
+
modTypes: ["class", "subclass"],
|
|
439
|
+
summary: "Every class/subclass table this mod owns needs a class-name-anchor ProgressionDescription.",
|
|
440
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
441
|
+
appliesToDescription: "once per project, over every ClassDescription this mod authors",
|
|
442
|
+
needs: { project: true },
|
|
443
|
+
cost: "edit",
|
|
444
|
+
severity: "block",
|
|
445
|
+
evidence: ["bg3.progressions.class-anchor-description", "bg3.progressions.subclass-anchor-description"],
|
|
446
|
+
async run(ctx) {
|
|
447
|
+
const idx = await collectProjectLsx(ctx);
|
|
448
|
+
const anchors = nodesOf(idx, "ProgressionDescription");
|
|
449
|
+
const findings = [];
|
|
450
|
+
for (const ref of nodesOf(idx, "ClassDescription")) {
|
|
451
|
+
const name = attr(ref.node, "Name");
|
|
452
|
+
const tableUUID = attr(ref.node, "ProgressionTableUUID");
|
|
453
|
+
if (!name || !tableUUID)
|
|
454
|
+
continue;
|
|
455
|
+
const found = anchors.some((a) => {
|
|
456
|
+
const exactMatch = attr(a.node, "ExactMatch");
|
|
457
|
+
const hidden = (attr(a.node, "Hidden") ?? "false").toLowerCase();
|
|
458
|
+
const scopedTable = attr(a.node, "TableUUID") ?? attr(a.node, "ProgressionTableUUID");
|
|
459
|
+
return exactMatch === name && hidden === "false" && (scopedTable === undefined || scopedTable === tableUUID);
|
|
460
|
+
});
|
|
461
|
+
if (!found) {
|
|
462
|
+
findings.push({
|
|
463
|
+
validatorId: this.id,
|
|
464
|
+
severity: "block",
|
|
465
|
+
file: ref.file,
|
|
466
|
+
message: `ClassDescription "${name}" has no anchor ProgressionDescription (ExactMatch="${name}", Hidden=false, ` +
|
|
467
|
+
`scoped to table "${tableUUID}") — without it the CC/level-up viewmodel never creates the feature ` +
|
|
468
|
+
`group and nothing boost-derived renders, not even proficiencies.`,
|
|
469
|
+
fix: `Add a ProgressionDescription with ExactMatch="${name}", Hidden="False", TableUUID="${tableUUID}".`,
|
|
470
|
+
evidence: this.evidence,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return findings;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
const BUILTIN_ACTION_RESOURCES = new Set([
|
|
478
|
+
"SpellSlot",
|
|
479
|
+
"ActionPoint",
|
|
480
|
+
"BonusActionPoint",
|
|
481
|
+
"Movement",
|
|
482
|
+
"KiPoint",
|
|
483
|
+
"SorceryPoint",
|
|
484
|
+
"SuperiorityDie",
|
|
485
|
+
"ChannelDivinity",
|
|
486
|
+
"ChannelOath",
|
|
487
|
+
"WildShape",
|
|
488
|
+
"LayOnHandsCharge",
|
|
489
|
+
"HitDice",
|
|
490
|
+
"Rage",
|
|
491
|
+
"ArcaneRecoveryPoint",
|
|
492
|
+
]);
|
|
493
|
+
const actionresourceDefined = {
|
|
494
|
+
id: "bg3.progression.actionresource-defined",
|
|
495
|
+
game: GAME,
|
|
496
|
+
groups: ["progression"],
|
|
497
|
+
modTypes: ["class", "subclass", "feat"],
|
|
498
|
+
summary: "ActionResource(...) boosts must name a resource this mod (or vanilla) defines; defined resources must be granted.",
|
|
499
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
500
|
+
appliesToDescription: "once per project, over Boosts strings and ActionResourceDefinitions this mod authors",
|
|
501
|
+
needs: { project: true },
|
|
502
|
+
cost: "edit",
|
|
503
|
+
severity: "block",
|
|
504
|
+
evidence: [
|
|
505
|
+
"bg3.feats.classes.actionresourcedefinition-required-fields",
|
|
506
|
+
"bg3.feats.classes.actionresource-granted-via-boosts",
|
|
507
|
+
],
|
|
508
|
+
async run(ctx) {
|
|
509
|
+
const idx = await collectProjectLsx(ctx);
|
|
510
|
+
const defined = new Set(nodesOf(idx, "ActionResourceDefinition").map((r) => attr(r.node, "Name")).filter((v) => !!v));
|
|
511
|
+
const findings = [];
|
|
512
|
+
if (defined.size === 0)
|
|
513
|
+
return findings;
|
|
514
|
+
const used = new Map();
|
|
515
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
516
|
+
const nodeTable = attr(ref.node, "TableUUID");
|
|
517
|
+
const isInjection = nodeTable !== undefined && (await isVanillaTableInjection(ctx, attr(ref.node, "Name"), nodeTable));
|
|
518
|
+
const boosts = attr(ref.node, "Boosts") ?? "";
|
|
519
|
+
for (const m of boosts.matchAll(/ActionResource\(([^,)]+)/g)) {
|
|
520
|
+
const resourceName = m[1].trim();
|
|
521
|
+
const list = used.get(resourceName) ?? [];
|
|
522
|
+
list.push(attr(ref.node, "Name") ?? "?");
|
|
523
|
+
used.set(resourceName, list);
|
|
524
|
+
if (isInjection)
|
|
525
|
+
continue;
|
|
526
|
+
if (!defined.has(resourceName) && !BUILTIN_ACTION_RESOURCES.has(resourceName)) {
|
|
527
|
+
findings.push({
|
|
528
|
+
validatorId: this.id,
|
|
529
|
+
severity: "block",
|
|
530
|
+
file: ref.file,
|
|
531
|
+
message: `Progression's Boosts grants ActionResource(${resourceName},...), which is not defined in this mod's ActionResourceDefinitions.lsx.`,
|
|
532
|
+
fix: `Define an ActionResourceDefinition named "${resourceName}", or fix the boost.`,
|
|
533
|
+
evidence: this.evidence,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const ref of nodesOf(idx, "ActionResourceDefinition")) {
|
|
539
|
+
const name = attr(ref.node, "Name");
|
|
540
|
+
if (name && !used.has(name)) {
|
|
541
|
+
findings.push({
|
|
542
|
+
validatorId: this.id,
|
|
543
|
+
severity: "warn",
|
|
544
|
+
file: ref.file,
|
|
545
|
+
message: `ActionResourceDefinition "${name}" is defined but never granted via ActionResource(${name},...) in any Boosts — it is just floating around.`,
|
|
546
|
+
fix: `Grant it from a Progression's Boosts, or remove the definition.`,
|
|
547
|
+
evidence: this.evidence,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return findings;
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
const RACE_BLOCK_FIELDS = ["ProgressionTableUUID", "UUID"];
|
|
555
|
+
const RACE_WARN_FIELDS = ["Description", "DisplayName", "DisplayTypeUUID", "Name", "ParentGuid", "RaceSoundSwitch"];
|
|
556
|
+
const raceRequiredFields = {
|
|
557
|
+
id: "bg3.progression.race-required-fields",
|
|
558
|
+
game: GAME,
|
|
559
|
+
groups: ["progression"],
|
|
560
|
+
modTypes: ["race"],
|
|
561
|
+
summary: "A Races.lsx Race node carries its required fields; RootTemplates with a Race attribute must match one.",
|
|
562
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
563
|
+
appliesToDescription: "once per project, over every Race node this mod authors",
|
|
564
|
+
needs: { project: true },
|
|
565
|
+
cost: "edit",
|
|
566
|
+
severity: "block",
|
|
567
|
+
evidence: ["bg3.races.progression.race-root-required-fields", "bg3.races.progression.roottemplate-race-attribute-must-match"],
|
|
568
|
+
async run(ctx) {
|
|
569
|
+
const idx = await collectProjectLsx(ctx);
|
|
570
|
+
const races = nodesOf(idx, "Race");
|
|
571
|
+
const findings = [];
|
|
572
|
+
const raceUuids = new Set(races.map((r) => attr(r.node, "UUID")).filter((v) => !!v));
|
|
573
|
+
for (const ref of races) {
|
|
574
|
+
const name = attr(ref.node, "Name") ?? "?";
|
|
575
|
+
for (const field of RACE_BLOCK_FIELDS) {
|
|
576
|
+
if (!attr(ref.node, field)) {
|
|
577
|
+
findings.push({
|
|
578
|
+
validatorId: this.id,
|
|
579
|
+
severity: "block",
|
|
580
|
+
file: ref.file,
|
|
581
|
+
message: `Race "${name}" is missing required field "${field}".`,
|
|
582
|
+
fix: `Add ${field} to the Race node.`,
|
|
583
|
+
evidence: this.evidence,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const field of RACE_WARN_FIELDS) {
|
|
588
|
+
if (!attr(ref.node, field)) {
|
|
589
|
+
findings.push({
|
|
590
|
+
validatorId: this.id,
|
|
591
|
+
severity: "warn",
|
|
592
|
+
file: ref.file,
|
|
593
|
+
message: `Race "${name}" is missing recommended field "${field}".`,
|
|
594
|
+
fix: `Add ${field} to the Race node.`,
|
|
595
|
+
evidence: this.evidence,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (raceUuids.size > 0) {
|
|
601
|
+
for (const ref of nodesOf(idx, "GameObjects")) {
|
|
602
|
+
const race = attr(ref.node, "Race");
|
|
603
|
+
if (!race)
|
|
604
|
+
continue;
|
|
605
|
+
if (!raceUuids.has(race)) {
|
|
606
|
+
findings.push({
|
|
607
|
+
validatorId: this.id,
|
|
608
|
+
severity: "block",
|
|
609
|
+
file: ref.file,
|
|
610
|
+
message: `RootTemplate "${attr(ref.node, "MapKey") ?? "?"}"'s Race attribute "${race}" does not match any Race UUID this mod defines.`,
|
|
611
|
+
fix: `Set Race to one of this mod's Race UUIDs, or remove the attribute if this template inherits its race via ParentTemplateId.`,
|
|
612
|
+
evidence: this.evidence,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return findings;
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
function decodeVersion64(raw) {
|
|
621
|
+
let v;
|
|
622
|
+
try {
|
|
623
|
+
v = BigInt(raw);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
if (v < 0n)
|
|
629
|
+
return undefined;
|
|
630
|
+
return { major: v >> 55n, minor: (v >> 47n) & 0xffn, revision: (v >> 31n) & 0xffffn, build: v & 0x7fffffffn };
|
|
631
|
+
}
|
|
632
|
+
const metaVersion64 = {
|
|
633
|
+
id: "bg3.progression.meta-version64",
|
|
634
|
+
game: GAME,
|
|
635
|
+
groups: ["progression"],
|
|
636
|
+
modTypes: ["class", "subclass", "race", "feat", "packaging"],
|
|
637
|
+
summary: "meta.lsx's Version64 decodes to a plausible tuple and agrees with modwright.json's version.",
|
|
638
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
639
|
+
appliesToDescription: "once per project, over meta.lsx's ModuleInfo/Version64",
|
|
640
|
+
needs: { project: true },
|
|
641
|
+
cost: "edit",
|
|
642
|
+
severity: "warn",
|
|
643
|
+
evidence: ["bg3.encoding.version64-format"],
|
|
644
|
+
async run(ctx) {
|
|
645
|
+
const idx = await collectProjectLsx(ctx);
|
|
646
|
+
const findings = [];
|
|
647
|
+
const declaredVersion = ctx.project.project.version;
|
|
648
|
+
for (const ref of nodesOf(idx, "ModuleInfo")) {
|
|
649
|
+
const raw = attr(ref.node, "Version64");
|
|
650
|
+
if (!raw)
|
|
651
|
+
continue;
|
|
652
|
+
const decoded = decodeVersion64(raw);
|
|
653
|
+
if (!decoded) {
|
|
654
|
+
findings.push({
|
|
655
|
+
validatorId: this.id,
|
|
656
|
+
severity: "warn",
|
|
657
|
+
file: ref.file,
|
|
658
|
+
message: `meta.lsx Version64="${raw}" does not decode to a plausible (major,minor,revision,build) tuple.`,
|
|
659
|
+
fix: `Encode Version64 as (major<<55)|(minor<<47)|(revision<<31)|build.`,
|
|
660
|
+
evidence: this.evidence,
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (!declaredVersion)
|
|
665
|
+
continue;
|
|
666
|
+
const parts = declaredVersion.split(".").map((p) => Number(p));
|
|
667
|
+
if (parts.length < 3 || parts.slice(0, 3).some((p) => Number.isNaN(p)))
|
|
668
|
+
continue;
|
|
669
|
+
const [major, minor, revision] = parts;
|
|
670
|
+
if (BigInt(major) !== decoded.major || BigInt(minor) !== decoded.minor || BigInt(revision) !== decoded.revision) {
|
|
671
|
+
findings.push({
|
|
672
|
+
validatorId: this.id,
|
|
673
|
+
severity: "warn",
|
|
674
|
+
file: ref.file,
|
|
675
|
+
message: `meta.lsx Version64="${raw}" decodes to ${decoded.major}.${decoded.minor}.${decoded.revision}.${decoded.build}, ` +
|
|
676
|
+
`which disagrees with modwright.json's version "${declaredVersion}" in major/minor/revision.`,
|
|
677
|
+
fix: `Re-encode Version64 for ${major}.${minor}.${revision}.<build>, or update modwright.json's version.`,
|
|
678
|
+
evidence: this.evidence,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return findings;
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
const noRemovespells = {
|
|
686
|
+
id: "bg3.progression.no-removespells",
|
|
687
|
+
game: GAME,
|
|
688
|
+
groups: ["progression"],
|
|
689
|
+
modTypes: ["race", "class", "subclass", "feat"],
|
|
690
|
+
summary: "RemoveSpells has no vanilla precedent; flag it rather than assume it works.",
|
|
691
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
692
|
+
appliesToDescription: "once per project, over every Progression node's Selectors/Boosts",
|
|
693
|
+
needs: { project: true },
|
|
694
|
+
cost: "edit",
|
|
695
|
+
severity: "warn",
|
|
696
|
+
evidence: ["bg3.progressions.no-spell-replacement-idiom"],
|
|
697
|
+
async run(ctx) {
|
|
698
|
+
const idx = await collectProjectLsx(ctx);
|
|
699
|
+
const findings = [];
|
|
700
|
+
for (const ref of nodesOf(idx, "Progression")) {
|
|
701
|
+
for (const field of ["Selectors", "Boosts"]) {
|
|
702
|
+
const value = attr(ref.node, field);
|
|
703
|
+
if (value?.includes("RemoveSpells(")) {
|
|
704
|
+
findings.push({
|
|
705
|
+
validatorId: this.id,
|
|
706
|
+
severity: "warn",
|
|
707
|
+
file: ref.file,
|
|
708
|
+
message: `Progression "${attr(ref.node, "Name") ?? "?"}"'s ${field} uses RemoveSpells(...), which has zero ` +
|
|
709
|
+
`occurrences across the vanilla stats + progressions extract — there is no in-progression spell-` +
|
|
710
|
+
`replacement idiom; ship the upgrade additively instead.`,
|
|
711
|
+
fix: `Grant the replacement additively (keep the original spell) instead of using RemoveSpells.`,
|
|
712
|
+
evidence: this.evidence,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return findings;
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
export const PROGRESSION_VALIDATORS = [
|
|
721
|
+
tableuuidMatchesOwner,
|
|
722
|
+
level1NodePresent,
|
|
723
|
+
typeMatchesOwner,
|
|
724
|
+
subclassRegisteredInParent,
|
|
725
|
+
spelllistSelectorResolves,
|
|
726
|
+
spelllistMembersExist,
|
|
727
|
+
passivesExist,
|
|
728
|
+
addspellsArgSlots,
|
|
729
|
+
classAnchorDescription,
|
|
730
|
+
actionresourceDefined,
|
|
731
|
+
raceRequiredFields,
|
|
732
|
+
metaVersion64,
|
|
733
|
+
noRemovespells,
|
|
734
|
+
];
|