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,1001 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { walk } from "../../../core/fsutil.js";
|
|
3
|
+
import { parseLsx, walkLsxNodes } from "../index/parsers/lsx.js";
|
|
4
|
+
import { parseStats } from "../index/parsers/stats.js";
|
|
5
|
+
export const GAME = "baldursgate3";
|
|
6
|
+
export async function collectProjectFiles(project, pattern) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const source of project.sources) {
|
|
9
|
+
if (!source.exists)
|
|
10
|
+
continue;
|
|
11
|
+
const walked = await walk(source.absPath, { limit: 50_000 });
|
|
12
|
+
for (const rel of walked.files) {
|
|
13
|
+
if (pattern.test(rel))
|
|
14
|
+
out.push({ abs: path.join(source.absPath, rel), rel, sourceId: source.id });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
async function parseLsxFile(ctx, file) {
|
|
20
|
+
try {
|
|
21
|
+
return parseLsx(await ctx.readText(file.abs), file.rel);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function parseStatsFile(ctx, file) {
|
|
28
|
+
try {
|
|
29
|
+
return parseStats(await ctx.readText(file.abs), file.rel);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function collectProjectLsx(ctx) {
|
|
36
|
+
const files = await collectProjectFiles(ctx.project, /\.lsx$/i);
|
|
37
|
+
const byId = new Map();
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const doc = await parseLsxFile(ctx, file);
|
|
40
|
+
if (!doc)
|
|
41
|
+
continue;
|
|
42
|
+
for (const region of doc.regions) {
|
|
43
|
+
walkLsxNodes(region.root, (node) => {
|
|
44
|
+
const list = byId.get(node.id);
|
|
45
|
+
const ref = { node, file: file.rel, region: region.id };
|
|
46
|
+
if (list)
|
|
47
|
+
list.push(ref);
|
|
48
|
+
else
|
|
49
|
+
byId.set(node.id, [ref]);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { byId };
|
|
54
|
+
}
|
|
55
|
+
export function nodesOf(idx, id) {
|
|
56
|
+
return idx.byId.get(id) ?? [];
|
|
57
|
+
}
|
|
58
|
+
export function attr(node, id) {
|
|
59
|
+
const v = node.attributes[id]?.value;
|
|
60
|
+
return v === undefined || v === "" ? undefined : v;
|
|
61
|
+
}
|
|
62
|
+
export function child(node, id) {
|
|
63
|
+
return node.children.find((c) => c.id === id);
|
|
64
|
+
}
|
|
65
|
+
export function children(node, id) {
|
|
66
|
+
return node.children.filter((c) => c.id === id);
|
|
67
|
+
}
|
|
68
|
+
export async function collectAllStatEntries(ctx) {
|
|
69
|
+
const files = await collectProjectFiles(ctx.project, /(^|\/)Stats\/Generated\/Data\/[^/]+\.txt$/i);
|
|
70
|
+
const byName = new Map();
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
for (const entry of await parseStatsFile(ctx, file))
|
|
73
|
+
byName.set(entry.name, entry);
|
|
74
|
+
}
|
|
75
|
+
return byName;
|
|
76
|
+
}
|
|
77
|
+
export const ARMOR_WEAPON_STATS = /(^|\/)Stats\/Generated\/Data\/(Armor|Weapon)\.txt$/i;
|
|
78
|
+
export async function collectArmorWeaponEntries(ctx) {
|
|
79
|
+
const files = await collectProjectFiles(ctx.project, ARMOR_WEAPON_STATS);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const statKind = /Weapon\.txt$/i.test(file.rel) ? "Weapon" : "Armor";
|
|
83
|
+
for (const entry of await parseStatsFile(ctx, file))
|
|
84
|
+
out.push({ ...entry, statKind });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export async function collectRootTemplates(idx) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const ref of nodesOf(idx, "GameObjects")) {
|
|
91
|
+
const mapKey = attr(ref.node, "MapKey");
|
|
92
|
+
if (!mapKey)
|
|
93
|
+
continue;
|
|
94
|
+
const equipment = child(ref.node, "Equipment");
|
|
95
|
+
out.push({
|
|
96
|
+
mapKey,
|
|
97
|
+
name: attr(ref.node, "Name"),
|
|
98
|
+
visualTemplate: attr(ref.node, "VisualTemplate"),
|
|
99
|
+
equipmentVisualSet: equipment ? attr(equipment, "VisualSet") : undefined,
|
|
100
|
+
parentTemplateId: attr(ref.node, "ParentTemplateId"),
|
|
101
|
+
race: attr(ref.node, "Race"),
|
|
102
|
+
icon: attr(ref.node, "Icon"),
|
|
103
|
+
file: ref.file,
|
|
104
|
+
node: ref.node,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
export function collectBankResources(idx) {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const ref of nodesOf(idx, "Resource")) {
|
|
112
|
+
if (ref.region !== "VisualBank" && ref.region !== "MaterialBank" && ref.region !== "TextureBank")
|
|
113
|
+
continue;
|
|
114
|
+
out.push({ region: ref.region, id: attr(ref.node, "ID"), name: attr(ref.node, "Name"), file: ref.file, node: ref.node });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
export async function detectPartialTree(ctx) {
|
|
119
|
+
const missing = ctx.project.sources.filter((s) => !s.exists);
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
return (`modwright.json declares source(s) ${missing.map((s) => `"${s.id}" (${s.path})`).join(", ")} ` +
|
|
122
|
+
`not present on disk here; this checkout is a partial tree, so a name that looks undefined may ` +
|
|
123
|
+
`only be missing because its source isn't checked out in this environment.`);
|
|
124
|
+
}
|
|
125
|
+
const checkedMods = new Set();
|
|
126
|
+
for (const source of ctx.project.sources) {
|
|
127
|
+
if (!source.exists)
|
|
128
|
+
continue;
|
|
129
|
+
const rel = path.relative(ctx.project.root, source.absPath).split(path.sep);
|
|
130
|
+
if (rel[0] !== "Public" || !rel[1])
|
|
131
|
+
continue;
|
|
132
|
+
const modName = rel[1];
|
|
133
|
+
if (checkedMods.has(modName))
|
|
134
|
+
continue;
|
|
135
|
+
checkedMods.add(modName);
|
|
136
|
+
const metaPath = path.join(ctx.project.root, "Mods", modName, "meta.lsx");
|
|
137
|
+
if (!(await ctx.exists(metaPath))) {
|
|
138
|
+
return (`source "${source.id}" is a Public/${modName} tree with no matching Mods/${modName}/meta.lsx ` +
|
|
139
|
+
`in this project; the module tree looks partial (missing the Mods/ half of the mod).`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export function skipFinding(validatorId, project, reason, evidence) {
|
|
145
|
+
return [{ validatorId, severity: "info", file: project.file, message: `Skipped: ${reason}`, evidence }];
|
|
146
|
+
}
|
|
147
|
+
export function isProjectTarget(target) {
|
|
148
|
+
return target.kind === "project";
|
|
149
|
+
}
|
|
150
|
+
async function resolveStatName(ctx, name, byName) {
|
|
151
|
+
if (byName.has(name))
|
|
152
|
+
return true;
|
|
153
|
+
if (!ctx.store)
|
|
154
|
+
return false;
|
|
155
|
+
const res = await ctx.lookup("stat", { name });
|
|
156
|
+
return res.count > 0;
|
|
157
|
+
}
|
|
158
|
+
async function resolveVanillaBank(ctx, id, expectedRegion) {
|
|
159
|
+
if (!ctx.store)
|
|
160
|
+
return false;
|
|
161
|
+
const res = await ctx.lookup("guid", { uuid: id });
|
|
162
|
+
return res.rows.some((r) => r.kind === "Resource" && r.region === expectedRegion);
|
|
163
|
+
}
|
|
164
|
+
async function resolveBankId(ctx, id, banks, expectedRegion) {
|
|
165
|
+
if (banks.some((b) => b.id === id))
|
|
166
|
+
return true;
|
|
167
|
+
return resolveVanillaBank(ctx, id, expectedRegion);
|
|
168
|
+
}
|
|
169
|
+
function hasModDependency(project) {
|
|
170
|
+
return (project.project.dependencies ?? []).some((d) => d.kind === "mod");
|
|
171
|
+
}
|
|
172
|
+
const statsRootTemplateLink = {
|
|
173
|
+
id: "bg3.items.stats-roottemplate-link",
|
|
174
|
+
game: GAME,
|
|
175
|
+
groups: ["items"],
|
|
176
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
177
|
+
summary: "Armor.txt/Weapon.txt data \"RootTemplate\" must link two ways to a RootTemplate GameObjects node.",
|
|
178
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
179
|
+
appliesToDescription: "once per project, over every Armor.txt/Weapon.txt entry",
|
|
180
|
+
needs: { project: true },
|
|
181
|
+
cost: "edit",
|
|
182
|
+
severity: "block",
|
|
183
|
+
evidence: [
|
|
184
|
+
"bg3.items.visualbank.roottemplate-mapkey-links-stats-entry",
|
|
185
|
+
"bg3.weapons.summoned-custom-recipe",
|
|
186
|
+
],
|
|
187
|
+
async run(ctx) {
|
|
188
|
+
const entries = await collectArmorWeaponEntries(ctx);
|
|
189
|
+
const idx = await collectProjectLsx(ctx);
|
|
190
|
+
const templates = await collectRootTemplates(idx);
|
|
191
|
+
const findings = [];
|
|
192
|
+
const withRootTemplateField = entries.filter((e) => e.fields.RootTemplate);
|
|
193
|
+
if (withRootTemplateField.length > 0 && templates.length === 0) {
|
|
194
|
+
return skipFinding(this.id, ctx.project, "no RootTemplates .lsx found in this project (they may ship only as a compiled _merged.lsf, which this rule cannot read without divine)", this.evidence);
|
|
195
|
+
}
|
|
196
|
+
const compiledLsf = await collectProjectFiles(ctx.project, /RootTemplates[\\/][^\\/]*\.lsf$/i);
|
|
197
|
+
const unverifiable = [];
|
|
198
|
+
for (const entry of withRootTemplateField) {
|
|
199
|
+
const rootTemplate = entry.fields.RootTemplate;
|
|
200
|
+
const template = templates.find((t) => t.mapKey === rootTemplate);
|
|
201
|
+
if (!template) {
|
|
202
|
+
if (compiledLsf.length > 0) {
|
|
203
|
+
unverifiable.push({ name: entry.name, rootTemplate });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
findings.push({
|
|
207
|
+
validatorId: this.id,
|
|
208
|
+
severity: "block",
|
|
209
|
+
file: entry.file,
|
|
210
|
+
line: entry.line,
|
|
211
|
+
message: `${entry.statKind} entry "${entry.name}"'s data "RootTemplate" "${rootTemplate}" does not match ` +
|
|
212
|
+
`any RootTemplate GameObjects node's MapKey in this project.`,
|
|
213
|
+
fix: `Add a GameObjects node with MapKey="${rootTemplate}" under RootTemplates, or fix the stats entry's RootTemplate value.`,
|
|
214
|
+
evidence: this.evidence,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (template.name !== entry.name) {
|
|
219
|
+
findings.push({
|
|
220
|
+
validatorId: this.id,
|
|
221
|
+
severity: "block",
|
|
222
|
+
file: entry.file,
|
|
223
|
+
line: entry.line,
|
|
224
|
+
message: `RootTemplate "${rootTemplate}" (Name="${template.name ?? ""}") does not equal the stats entry name ` +
|
|
225
|
+
`"${entry.name}" that references it via data "RootTemplate" — the two-way link is broken.`,
|
|
226
|
+
fix: `Set the RootTemplate GameObjects node's Name to "${entry.name}", or point the stats entry at the correct RootTemplate.`,
|
|
227
|
+
related: [template.file],
|
|
228
|
+
evidence: this.evidence,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (unverifiable.length > 0) {
|
|
233
|
+
const shown = unverifiable.slice(0, 10).map((u) => `${u.name} (${u.rootTemplate})`).join(", ");
|
|
234
|
+
findings.push({
|
|
235
|
+
validatorId: this.id,
|
|
236
|
+
severity: "info",
|
|
237
|
+
file: ctx.project.file,
|
|
238
|
+
message: `${unverifiable.length} Armor/Weapon entr${unverifiable.length === 1 ? "y" : "ies"} reference a RootTemplate that no project .lsx defines, ` +
|
|
239
|
+
`but this project also ships compiled ${compiledLsf.map((f) => f.rel).join(", ")}, which this rule cannot read; ` +
|
|
240
|
+
`they may well be defined there. Not verified: ${shown}${unverifiable.length > 10 ? ", ..." : ""}.`,
|
|
241
|
+
fix: `To verify, convert the compiled file to .lsx (the convert tool's bg3.lsf-to-lsx) and check each MapKey, or keep the project's RootTemplates as .lsx sources.`,
|
|
242
|
+
related: compiledLsf.map((f) => f.rel),
|
|
243
|
+
evidence: this.evidence,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return findings;
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const roottemplateVisualLink = {
|
|
250
|
+
id: "bg3.items.roottemplate-visual-link",
|
|
251
|
+
game: GAME,
|
|
252
|
+
groups: ["items"],
|
|
253
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
254
|
+
summary: "A RootTemplate's VisualTemplate/Equipment visual UUIDs must resolve to a VisualBank Resource.",
|
|
255
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
256
|
+
appliesToDescription: "once per project, over every RootTemplate GameObjects node",
|
|
257
|
+
needs: { project: true, index: true },
|
|
258
|
+
cost: "edit",
|
|
259
|
+
severity: "block",
|
|
260
|
+
evidence: [
|
|
261
|
+
"bg3.items.visualbank.visualbank-id-links-roottemplate",
|
|
262
|
+
"bg3.items.visualbank.weapon-loot-model-equals-item-model",
|
|
263
|
+
"bg3.loadorder.external-guid-dependencies",
|
|
264
|
+
],
|
|
265
|
+
async run(ctx) {
|
|
266
|
+
const idx = await collectProjectLsx(ctx);
|
|
267
|
+
const templates = await collectRootTemplates(idx);
|
|
268
|
+
const banks = collectBankResources(idx);
|
|
269
|
+
const armorWeapon = await collectArmorWeaponEntries(ctx);
|
|
270
|
+
const findings = [];
|
|
271
|
+
const visualBanks = banks.filter((b) => b.region === "VisualBank");
|
|
272
|
+
const dependencyDeclared = hasModDependency(ctx.project);
|
|
273
|
+
for (const template of templates) {
|
|
274
|
+
const isWeapon = armorWeapon.some((e) => e.statKind === "Weapon" && e.fields.RootTemplate === template.mapKey);
|
|
275
|
+
for (const [label, uuid] of [
|
|
276
|
+
["VisualTemplate", template.visualTemplate],
|
|
277
|
+
["Equipment/VisualSet", template.equipmentVisualSet],
|
|
278
|
+
]) {
|
|
279
|
+
if (!uuid)
|
|
280
|
+
continue;
|
|
281
|
+
if (await resolveBankId(ctx, uuid, visualBanks, "VisualBank"))
|
|
282
|
+
continue;
|
|
283
|
+
const message = dependencyDeclared
|
|
284
|
+
? `RootTemplate "${template.mapKey}" (${template.name ?? "?"})'s ${label} "${uuid}" does not resolve to any ` +
|
|
285
|
+
`VisualBank Resource in this project or the vanilla index — it may be defined by a declared mod ` +
|
|
286
|
+
`dependency (bg3.loadorder.external-guid-dependencies): verify load order, or fix the UUID if it's a typo.`
|
|
287
|
+
: `RootTemplate "${template.mapKey}" (${template.name ?? "?"})'s ${label} "${uuid}" does not resolve to any ` +
|
|
288
|
+
`VisualBank Resource in this project or the vanilla index.`;
|
|
289
|
+
findings.push({
|
|
290
|
+
validatorId: this.id,
|
|
291
|
+
severity: "block",
|
|
292
|
+
file: template.file,
|
|
293
|
+
message,
|
|
294
|
+
fix: `Add a VisualBank Resource with ID="${uuid}", declare the owning mod as a "mod" dependency in modwright.json, or fix the UUID.`,
|
|
295
|
+
evidence: this.evidence,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (isWeapon && template.visualTemplate && template.equipmentVisualSet && template.visualTemplate !== template.equipmentVisualSet) {
|
|
299
|
+
findings.push({
|
|
300
|
+
validatorId: this.id,
|
|
301
|
+
severity: "block",
|
|
302
|
+
file: template.file,
|
|
303
|
+
message: `Weapon RootTemplate "${template.mapKey}" (${template.name ?? "?"})'s VisualTemplate ` +
|
|
304
|
+
`("${template.visualTemplate}") and equipped visual ("${template.equipmentVisualSet}") differ — for a ` +
|
|
305
|
+
`weapon the loot model and the item model must be the same UUID.`,
|
|
306
|
+
fix: `Set Equipment/VisualSet to the same UUID as VisualTemplate ("${template.visualTemplate}").`,
|
|
307
|
+
evidence: this.evidence,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return findings;
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
const visualMaterialTextureChain = {
|
|
315
|
+
id: "bg3.items.visual-material-texture-chain",
|
|
316
|
+
game: GAME,
|
|
317
|
+
groups: ["items"],
|
|
318
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
319
|
+
summary: "VisualBank Objects/MaterialID and MaterialBank Texture2DParameters/ID must chain to a real bank entry, in this project or the vanilla index.",
|
|
320
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
321
|
+
appliesToDescription: "once per project, over the merged VisualBank/MaterialBank/TextureBank tree",
|
|
322
|
+
needs: { project: true, index: true },
|
|
323
|
+
cost: "edit",
|
|
324
|
+
severity: "block",
|
|
325
|
+
evidence: ["bg3.items.visualbank.three-region-chain"],
|
|
326
|
+
async run(ctx) {
|
|
327
|
+
const idx = await collectProjectLsx(ctx);
|
|
328
|
+
const banks = collectBankResources(idx);
|
|
329
|
+
const materialBanks = banks.filter((b) => b.region === "MaterialBank");
|
|
330
|
+
const textureBanks = banks.filter((b) => b.region === "TextureBank");
|
|
331
|
+
const findings = [];
|
|
332
|
+
for (const visual of banks.filter((b) => b.region === "VisualBank")) {
|
|
333
|
+
for (const objects of children(visual.node, "Objects")) {
|
|
334
|
+
const materialId = attr(objects, "MaterialID");
|
|
335
|
+
if (!materialId)
|
|
336
|
+
continue;
|
|
337
|
+
const material = materialBanks.find((m) => m.id === materialId);
|
|
338
|
+
if (!material) {
|
|
339
|
+
if (await resolveVanillaBank(ctx, materialId, "MaterialBank"))
|
|
340
|
+
continue;
|
|
341
|
+
findings.push({
|
|
342
|
+
validatorId: this.id,
|
|
343
|
+
severity: "block",
|
|
344
|
+
file: visual.file,
|
|
345
|
+
message: `VisualBank Resource "${visual.name ?? visual.id ?? "?"}" (ID=${visual.id ?? "?"})'s Objects/MaterialID ` +
|
|
346
|
+
`"${materialId}" does not match any MaterialBank Resource ID in this project or the vanilla index.`,
|
|
347
|
+
fix: `Add a MaterialBank Resource with ID="${materialId}", or fix the MaterialID.`,
|
|
348
|
+
evidence: this.evidence,
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
for (const texParam of children(material.node, "Texture2DParameters")) {
|
|
353
|
+
const textureId = attr(texParam, "ID");
|
|
354
|
+
if (!textureId)
|
|
355
|
+
continue;
|
|
356
|
+
if (textureBanks.some((t) => t.id === textureId))
|
|
357
|
+
continue;
|
|
358
|
+
if (await resolveVanillaBank(ctx, textureId, "TextureBank"))
|
|
359
|
+
continue;
|
|
360
|
+
findings.push({
|
|
361
|
+
validatorId: this.id,
|
|
362
|
+
severity: "block",
|
|
363
|
+
file: material.file,
|
|
364
|
+
message: `MaterialBank Resource "${material.name ?? material.id ?? "?"}" (ID=${material.id ?? "?"})'s ` +
|
|
365
|
+
`Texture2DParameters ID "${textureId}" does not match any TextureBank Resource ID in this project or ` +
|
|
366
|
+
`the vanilla index.`,
|
|
367
|
+
fix: `Add a TextureBank Resource with ID="${textureId}", or fix the Texture2DParameters ID.`,
|
|
368
|
+
related: [visual.file],
|
|
369
|
+
evidence: this.evidence,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return findings;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
const ddsExtensionCase = {
|
|
378
|
+
id: "bg3.items.dds-extension-case",
|
|
379
|
+
game: GAME,
|
|
380
|
+
groups: ["items"],
|
|
381
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
382
|
+
summary: "TextureBank SourceFile must end in uppercase .DDS (lowercase .dds for an Icons_Items icon atlas) and exist at that exact path.",
|
|
383
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
384
|
+
appliesToDescription: "once per project, over every TextureBank Resource",
|
|
385
|
+
needs: { project: true },
|
|
386
|
+
cost: "edit",
|
|
387
|
+
severity: "block",
|
|
388
|
+
evidence: [
|
|
389
|
+
"bg3.items.visualbank.dds-extension-case-sensitive",
|
|
390
|
+
"bg3.ui.icons-atlas.atlas-path-must-match-textureatlaspath",
|
|
391
|
+
"bg3.se.config-required",
|
|
392
|
+
],
|
|
393
|
+
async run(ctx) {
|
|
394
|
+
const idx = await collectProjectLsx(ctx);
|
|
395
|
+
const banks = collectBankResources(idx);
|
|
396
|
+
const findings = [];
|
|
397
|
+
for (const texture of banks.filter((b) => b.region === "TextureBank")) {
|
|
398
|
+
const sourceFile = attr(texture.node, "SourceFile");
|
|
399
|
+
if (!sourceFile)
|
|
400
|
+
continue;
|
|
401
|
+
const isAtlas = attr(texture.node, "Template") === "Icons_Items";
|
|
402
|
+
const wantExt = isAtlas ? ".dds" : ".DDS";
|
|
403
|
+
if (!sourceFile.endsWith(wantExt)) {
|
|
404
|
+
findings.push({
|
|
405
|
+
validatorId: this.id,
|
|
406
|
+
severity: "block",
|
|
407
|
+
file: texture.file,
|
|
408
|
+
message: isAtlas
|
|
409
|
+
? `TextureBank Resource "${texture.name ?? texture.id ?? "?"}" is an icon atlas (Template "Icons_Items") ` +
|
|
410
|
+
`but its SourceFile "${sourceFile}" does not end in lowercase ".dds" — atlases are the one texture ` +
|
|
411
|
+
`class shipped lowercase (bg3.items.visualbank.dds-extension-case-sensitive, detail).`
|
|
412
|
+
: `TextureBank Resource "${texture.name ?? texture.id ?? "?"}"'s SourceFile "${sourceFile}" does not end ` +
|
|
413
|
+
`in uppercase ".DDS" — capitalisation is checked on the recorded string, since a lowercase ".dds" ` +
|
|
414
|
+
`file at the same path will not load in the packed pak.`,
|
|
415
|
+
fix: isAtlas
|
|
416
|
+
? `Rename the atlas file, the SourceFile value and GUI/<Atlas>.lsx's TextureAtlasPath to end in ".dds" (lowercase).`
|
|
417
|
+
: `Rename the file and the SourceFile value to end in ".DDS" (uppercase).`,
|
|
418
|
+
evidence: this.evidence,
|
|
419
|
+
});
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const abs = path.isAbsolute(sourceFile) ? sourceFile : path.join(ctx.project.root, sourceFile);
|
|
423
|
+
if (!(await ctx.exists(abs))) {
|
|
424
|
+
findings.push({
|
|
425
|
+
validatorId: this.id,
|
|
426
|
+
severity: "block",
|
|
427
|
+
file: texture.file,
|
|
428
|
+
message: `TextureBank Resource "${texture.name ?? texture.id ?? "?"}"'s SourceFile "${sourceFile}" does not exist at that exact path.`,
|
|
429
|
+
fix: `Add the texture file at "${sourceFile}" (relative to the project root), or fix the SourceFile value.`,
|
|
430
|
+
evidence: this.evidence,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return findings;
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
const MAP_ROLE_SUFFIXES = { BM: true, NM: false, PM: false, MSK: false };
|
|
438
|
+
const texturebankSrgb = {
|
|
439
|
+
id: "bg3.items.texturebank-srgb",
|
|
440
|
+
game: GAME,
|
|
441
|
+
groups: ["items"],
|
|
442
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
443
|
+
summary: "SRGB is True only on the basecolor (_BM) TextureBank entry; normal/physical/mask maps are False.",
|
|
444
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
445
|
+
appliesToDescription: "once per project, over TextureBank entries whose filename ends _BM/_NM/_PM/_MSK",
|
|
446
|
+
needs: { project: true },
|
|
447
|
+
cost: "edit",
|
|
448
|
+
severity: "warn",
|
|
449
|
+
evidence: ["bg3.items.visualbank.texturebank-srgb-basecolor-only"],
|
|
450
|
+
async run(ctx) {
|
|
451
|
+
const idx = await collectProjectLsx(ctx);
|
|
452
|
+
const banks = collectBankResources(idx);
|
|
453
|
+
const findings = [];
|
|
454
|
+
for (const texture of banks.filter((b) => b.region === "TextureBank")) {
|
|
455
|
+
const sourceFile = attr(texture.node, "SourceFile");
|
|
456
|
+
const srgb = attr(texture.node, "SRGB");
|
|
457
|
+
if (!sourceFile || srgb === undefined)
|
|
458
|
+
continue;
|
|
459
|
+
const stem = path.basename(sourceFile).replace(/\.[^.]+$/, "");
|
|
460
|
+
const suffix = Object.keys(MAP_ROLE_SUFFIXES).find((s) => stem.endsWith(`_${s}`));
|
|
461
|
+
if (!suffix)
|
|
462
|
+
continue;
|
|
463
|
+
const expected = MAP_ROLE_SUFFIXES[suffix];
|
|
464
|
+
const actual = srgb.toLowerCase() === "true";
|
|
465
|
+
if (actual !== expected) {
|
|
466
|
+
findings.push({
|
|
467
|
+
validatorId: this.id,
|
|
468
|
+
severity: "warn",
|
|
469
|
+
file: texture.file,
|
|
470
|
+
message: `TextureBank Resource "${sourceFile}" is a ${suffix} map but SRGB="${srgb}" ` +
|
|
471
|
+
`(expected ${expected ? "True (basecolor)" : "False (no color data)"}).`,
|
|
472
|
+
fix: `Set SRGB="${expected ? "True" : "False"}" on this Resource.`,
|
|
473
|
+
evidence: this.evidence,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return findings;
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
const OBJECT_CATEGORY = /object category\s+"([^"]+)"/g;
|
|
481
|
+
const treasuretableIPrefix = {
|
|
482
|
+
id: "bg3.items.treasuretable-i-prefix",
|
|
483
|
+
game: GAME,
|
|
484
|
+
groups: ["items"],
|
|
485
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
486
|
+
summary: "Every TreasureTable.txt `object category` line uses the I_ prefix and resolves to a stat entry or a known vanilla treasure category.",
|
|
487
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
488
|
+
appliesToDescription: "once per project, over every TreasureTable.txt `object category` line",
|
|
489
|
+
needs: { project: true, index: true },
|
|
490
|
+
cost: "edit",
|
|
491
|
+
severity: "block",
|
|
492
|
+
evidence: ["bg3.items.visualbank.treasuretable-i-prefix"],
|
|
493
|
+
async run(ctx) {
|
|
494
|
+
const files = await collectProjectFiles(ctx.project, /TreasureTable\.txt$/i);
|
|
495
|
+
const byName = await collectAllStatEntries(ctx);
|
|
496
|
+
const findings = [];
|
|
497
|
+
for (const file of files) {
|
|
498
|
+
const text = await ctx.readText(file.abs);
|
|
499
|
+
for (const m of text.matchAll(OBJECT_CATEGORY)) {
|
|
500
|
+
const value = m[1];
|
|
501
|
+
if (!value.startsWith("I_")) {
|
|
502
|
+
findings.push({
|
|
503
|
+
validatorId: this.id,
|
|
504
|
+
severity: "block",
|
|
505
|
+
file: file.abs,
|
|
506
|
+
message: `TreasureTable.txt object category "${value}" is missing the required "I_" item prefix.`,
|
|
507
|
+
fix: `Rename the reference to "I_${value}" and the item's stats entry to match.`,
|
|
508
|
+
evidence: this.evidence,
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const statName = value.slice(2);
|
|
513
|
+
if (await resolveStatName(ctx, statName, byName))
|
|
514
|
+
continue;
|
|
515
|
+
if (ctx.store) {
|
|
516
|
+
const treasureRes = await ctx.lookup("list", { kind: "TreasureCategory", name: value, limit: 1 });
|
|
517
|
+
if (treasureRes.count > 0)
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
findings.push({
|
|
521
|
+
validatorId: this.id,
|
|
522
|
+
severity: "block",
|
|
523
|
+
file: file.abs,
|
|
524
|
+
message: `TreasureTable.txt object category "${value}" names item "${statName}", which is not a defined stats ` +
|
|
525
|
+
`entry or a known vanilla treasure category in this mod or vanilla.`,
|
|
526
|
+
fix: `Define a stats entry named "${statName}", or fix the TreasureTable reference.`,
|
|
527
|
+
evidence: this.evidence,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return findings;
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
const iconMapkeyResolves = {
|
|
535
|
+
id: "bg3.items.icon-mapkey-resolves",
|
|
536
|
+
game: GAME,
|
|
537
|
+
groups: ["items"],
|
|
538
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
539
|
+
summary: "An item's Icon (a stats entry's data \"Icon\", or a RootTemplate's Icon attribute) must resolve to an IconUV MapKey in a mod atlas or the vanilla index.",
|
|
540
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
541
|
+
appliesToDescription: "once per project, over every Armor.txt/Weapon.txt entry's data \"Icon\" and every project RootTemplate's Icon attribute",
|
|
542
|
+
needs: { project: true, index: true },
|
|
543
|
+
cost: "edit",
|
|
544
|
+
severity: "warn",
|
|
545
|
+
evidence: [
|
|
546
|
+
"bg3.icons.spell-atlas-three-file-system",
|
|
547
|
+
"bg3.ui.icons-atlas.atlas-path-must-match-textureatlaspath",
|
|
548
|
+
"bg3.icons.item-tooltip-not-atlas",
|
|
549
|
+
],
|
|
550
|
+
async run(ctx) {
|
|
551
|
+
const entries = await collectArmorWeaponEntries(ctx);
|
|
552
|
+
const idx = await collectProjectLsx(ctx);
|
|
553
|
+
const iconKeys = new Set(nodesOf(idx, "IconUV").map((ref) => attr(ref.node, "MapKey")).filter((v) => !!v));
|
|
554
|
+
const findings = [];
|
|
555
|
+
const owners = [
|
|
556
|
+
...entries.map((entry) => ({ owner: `${entry.statKind} entry "${entry.name}"`, icon: entry.fields.Icon, file: entry.file, line: entry.line, where: 'data "Icon"' })),
|
|
557
|
+
...(await collectRootTemplates(idx)).map((t) => ({ owner: `RootTemplate "${t.name ?? t.mapKey}"`, icon: t.icon, file: t.file, where: "Icon attribute" })),
|
|
558
|
+
];
|
|
559
|
+
for (const entry of owners) {
|
|
560
|
+
const icon = entry.icon;
|
|
561
|
+
if (!icon)
|
|
562
|
+
continue;
|
|
563
|
+
if (iconKeys.has(icon))
|
|
564
|
+
continue;
|
|
565
|
+
let resolved = false;
|
|
566
|
+
if (ctx.store) {
|
|
567
|
+
const res = await ctx.lookup("guid", { name: icon });
|
|
568
|
+
resolved = res.rows.some((r) => r.kind === "IconUV");
|
|
569
|
+
}
|
|
570
|
+
if (!resolved) {
|
|
571
|
+
findings.push({
|
|
572
|
+
validatorId: this.id,
|
|
573
|
+
severity: "warn",
|
|
574
|
+
file: entry.file,
|
|
575
|
+
...(entry.line !== undefined ? { line: entry.line } : {}),
|
|
576
|
+
message: `${entry.owner}'s ${entry.where} "${icon}" does not resolve to an IconUV MapKey ` +
|
|
577
|
+
`in this mod's atlases or the vanilla index — it will show the blue fallback icon. Note: even a ` +
|
|
578
|
+
`resolved atlas hit does not give the item a tooltip image (bg3.icons.item-tooltip-not-atlas).`,
|
|
579
|
+
fix: `Add an IconUV MapKey="${icon}" to an atlas descriptor .lsx, or fix the Icon.`,
|
|
580
|
+
evidence: this.evidence,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return findings;
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
const parenttemplateidTypeMatch = {
|
|
588
|
+
id: "bg3.items.parenttemplateid-type-match",
|
|
589
|
+
game: GAME,
|
|
590
|
+
groups: ["items"],
|
|
591
|
+
modTypes: ["armor", "weapon", "accessory"],
|
|
592
|
+
summary: "A RootTemplate's ParentTemplateId must resolve, and the parent's item type (Slot) should match.",
|
|
593
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
594
|
+
appliesToDescription: "once per project, over RootTemplates that declare a ParentTemplateId",
|
|
595
|
+
needs: { project: true, index: true },
|
|
596
|
+
cost: "edit",
|
|
597
|
+
severity: "warn",
|
|
598
|
+
evidence: ["bg3.items.visualbank.parenttemplateid-inheritance-fallback"],
|
|
599
|
+
async run(ctx) {
|
|
600
|
+
const idx = await collectProjectLsx(ctx);
|
|
601
|
+
const templates = await collectRootTemplates(idx);
|
|
602
|
+
const stats = await collectArmorWeaponEntries(ctx);
|
|
603
|
+
const statByRootTemplate = new Map(stats.filter((s) => s.fields.RootTemplate).map((s) => [s.fields.RootTemplate, s]));
|
|
604
|
+
const findings = [];
|
|
605
|
+
for (const template of templates) {
|
|
606
|
+
if (!template.parentTemplateId)
|
|
607
|
+
continue;
|
|
608
|
+
const ownStat = statByRootTemplate.get(template.mapKey);
|
|
609
|
+
const ownSlot = ownStat?.fields.Slot;
|
|
610
|
+
if (!ownSlot)
|
|
611
|
+
continue;
|
|
612
|
+
let parentSlot;
|
|
613
|
+
const parentTemplate = templates.find((t) => t.mapKey === template.parentTemplateId);
|
|
614
|
+
if (parentTemplate?.name) {
|
|
615
|
+
parentSlot = statByRootTemplate.get(parentTemplate.mapKey)?.fields.Slot;
|
|
616
|
+
}
|
|
617
|
+
else if (ctx.store) {
|
|
618
|
+
const guidRes = await ctx.lookup("guid", { uuid: template.parentTemplateId });
|
|
619
|
+
const parentName = guidRes.rows[0]?.name;
|
|
620
|
+
if (parentName) {
|
|
621
|
+
const statRes = await ctx.lookup("stat", { name: parentName });
|
|
622
|
+
parentSlot = statRes.rows[0]?.fields?.Slot;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (!parentSlot)
|
|
626
|
+
continue;
|
|
627
|
+
if (parentSlot !== ownSlot) {
|
|
628
|
+
findings.push({
|
|
629
|
+
validatorId: this.id,
|
|
630
|
+
severity: "warn",
|
|
631
|
+
file: template.file,
|
|
632
|
+
message: `RootTemplate "${template.mapKey}" (Slot="${ownSlot}") has ParentTemplateId "${template.parentTemplateId}" ` +
|
|
633
|
+
`whose resolved item type is Slot="${parentSlot}" — an item silently inherits unset properties from its ` +
|
|
634
|
+
`ParentTemplateId, so a mismatched type here means it "will not know it is a ${ownSlot}".`,
|
|
635
|
+
fix: `Point ParentTemplateId at a RootTemplate of the same Slot type ("${ownSlot}").`,
|
|
636
|
+
evidence: this.evidence,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return findings;
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
const armorWeaponFields = {
|
|
644
|
+
id: "bg3.items.armor-weapon-fields",
|
|
645
|
+
game: GAME,
|
|
646
|
+
groups: ["items"],
|
|
647
|
+
modTypes: ["armor", "weapon"],
|
|
648
|
+
summary: "Armor/Weapon entries carry the fields their type requires, and `using` resolves.",
|
|
649
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
650
|
+
appliesToDescription: "once per project, over every Armor.txt/Weapon.txt entry",
|
|
651
|
+
needs: { project: true, index: true },
|
|
652
|
+
cost: "edit",
|
|
653
|
+
severity: "block",
|
|
654
|
+
evidence: [
|
|
655
|
+
"bg3.weapons.reach-is-weapon-property",
|
|
656
|
+
"bg3.weapons.no-new-base-weapon-type",
|
|
657
|
+
"bg3.practice.reference-checker-invaluable",
|
|
658
|
+
],
|
|
659
|
+
async run(ctx) {
|
|
660
|
+
const entries = await collectArmorWeaponEntries(ctx);
|
|
661
|
+
const byName = await collectAllStatEntries(ctx);
|
|
662
|
+
const findings = [];
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
if (!entry.fields.RootTemplate) {
|
|
665
|
+
findings.push({
|
|
666
|
+
validatorId: this.id,
|
|
667
|
+
severity: "block",
|
|
668
|
+
file: entry.file,
|
|
669
|
+
line: entry.line,
|
|
670
|
+
message: `${entry.statKind} entry "${entry.name}" has no data "RootTemplate" — it cannot spawn or equip.`,
|
|
671
|
+
fix: `Add data "RootTemplate" "<GameObjects MapKey>".`,
|
|
672
|
+
evidence: this.evidence,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (entry.statKind === "Weapon") {
|
|
676
|
+
if (!entry.fields["Proficiency Group"]) {
|
|
677
|
+
findings.push({
|
|
678
|
+
validatorId: this.id,
|
|
679
|
+
severity: "warn",
|
|
680
|
+
file: entry.file,
|
|
681
|
+
line: entry.line,
|
|
682
|
+
message: `Weapon entry "${entry.name}" has no data "Proficiency Group".`,
|
|
683
|
+
fix: `Add data "Proficiency Group" "<Group>" — there is no new base weapon type (bg3.weapons.no-new-base-weapon-type); fake one by combining an existing group with Weapon Properties.`,
|
|
684
|
+
evidence: this.evidence,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (!entry.fields["Weapon Properties"]) {
|
|
688
|
+
findings.push({
|
|
689
|
+
validatorId: this.id,
|
|
690
|
+
severity: "warn",
|
|
691
|
+
file: entry.file,
|
|
692
|
+
line: entry.line,
|
|
693
|
+
message: `Weapon entry "${entry.name}" has no data "Weapon Properties".`,
|
|
694
|
+
fix: `Add data "Weapon Properties" "<Properties>" (e.g. Reach is a Weapon Property, not a boost — bg3.weapons.reach-is-weapon-property).`,
|
|
695
|
+
evidence: this.evidence,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (entry.using) {
|
|
700
|
+
const resolved = await resolveStatName(ctx, entry.using, byName);
|
|
701
|
+
if (!resolved) {
|
|
702
|
+
findings.push({
|
|
703
|
+
validatorId: this.id,
|
|
704
|
+
severity: "block",
|
|
705
|
+
file: entry.file,
|
|
706
|
+
line: entry.line,
|
|
707
|
+
message: `${entry.statKind} entry "${entry.name}" uses parent "${entry.using}", which is not defined in this mod or vanilla.`,
|
|
708
|
+
fix: `Define "${entry.using}", or fix the using value.`,
|
|
709
|
+
evidence: this.evidence,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
let current = entry.using;
|
|
714
|
+
const seen = new Set([entry.name]);
|
|
715
|
+
let depth = 0;
|
|
716
|
+
while (current && depth < 10) {
|
|
717
|
+
if (seen.has(current)) {
|
|
718
|
+
findings.push({
|
|
719
|
+
validatorId: this.id,
|
|
720
|
+
severity: "block",
|
|
721
|
+
file: entry.file,
|
|
722
|
+
line: entry.line,
|
|
723
|
+
message: `${entry.statKind} entry "${entry.name}"'s using-chain cycles back to "${current}".`,
|
|
724
|
+
fix: `Break the using-chain cycle.`,
|
|
725
|
+
evidence: this.evidence,
|
|
726
|
+
});
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
seen.add(current);
|
|
730
|
+
current = byName.get(current)?.using;
|
|
731
|
+
depth++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return findings;
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
export const BODY_TYPE_KEYS = [
|
|
740
|
+
{ key: "7d73f501-f65e-46af-a13b-2cacf3985d05", label: "M (HUM_M)" },
|
|
741
|
+
{ key: "71180b76-5752-4a97-b71f-911a69197f58", label: "F (HUM_F)" },
|
|
742
|
+
{ key: "e39505f7-f576-4e70-a99e-8e29cd381a11", label: "M" },
|
|
743
|
+
{ key: "47c0315c-7dc6-4862-b39b-8bf3a10f8b54", label: "F" },
|
|
744
|
+
{ key: "f07faafa-0c6f-4f79-a049-70e96b23d51b", label: "M" },
|
|
745
|
+
{ key: "06aaae02-bb9e-4fa3-ac00-b08e13a5b0fa", label: "F" },
|
|
746
|
+
{ key: "abf674d2-2ea4-4a74-ade0-125429f69f83", label: "M" },
|
|
747
|
+
{ key: "b4a34ce7-41be-44d9-8486-938fe1472149", label: "F" },
|
|
748
|
+
{ key: "a933e2a8-aee1-4ecb-80d2-8f47b706f024", label: "M" },
|
|
749
|
+
{ key: "8f00cf38-4588-433a-8175-8acdbbf33f33", label: "F" },
|
|
750
|
+
{ key: "5640e766-aa53-428d-815b-6a0b4ef95aca", label: "M" },
|
|
751
|
+
{ key: "c491d027-4332-4fda-948f-4a3df6772baa", label: "F" },
|
|
752
|
+
{ key: "9a8bbeba-850c-402f-bac5-ff15696e6497", label: "M" },
|
|
753
|
+
{ key: "6d38f246-15cb-48b5-9b85-378016a7a78e", label: "F" },
|
|
754
|
+
];
|
|
755
|
+
const equipmentVisualsStructure = {
|
|
756
|
+
id: "bg3.items.equipment-visuals-structure",
|
|
757
|
+
game: GAME,
|
|
758
|
+
groups: ["items"],
|
|
759
|
+
modTypes: ["armor", "accessory"],
|
|
760
|
+
summary: "A worn item's Equipment/Visuals body-type map is a direct child of Equipment (not inside VisualSet) and maps all 14 body-type keys.",
|
|
761
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
762
|
+
appliesToDescription: "once per project, over every RootTemplate GameObjects node with an Equipment child",
|
|
763
|
+
needs: { project: true },
|
|
764
|
+
cost: "edit",
|
|
765
|
+
severity: "block",
|
|
766
|
+
evidence: [
|
|
767
|
+
"bg3.items.visualbank.equipment-visuals-is-direct-child-not-in-visualset",
|
|
768
|
+
"bg3.items.visualbank.equipment-visuals-fourteen-body-type-keys",
|
|
769
|
+
"bg3.items.visualbank.parentrace-zero-uuid-opts-out",
|
|
770
|
+
],
|
|
771
|
+
async run(ctx) {
|
|
772
|
+
const idx = await collectProjectLsx(ctx);
|
|
773
|
+
const templates = await collectRootTemplates(idx);
|
|
774
|
+
const findings = [];
|
|
775
|
+
for (const template of templates) {
|
|
776
|
+
const equipment = child(template.node, "Equipment");
|
|
777
|
+
if (!equipment)
|
|
778
|
+
continue;
|
|
779
|
+
const who = `RootTemplate "${template.name ?? template.mapKey}"`;
|
|
780
|
+
const visualSet = child(equipment, "VisualSet");
|
|
781
|
+
const visuals = child(equipment, "Visuals");
|
|
782
|
+
if (visualSet && child(visualSet, "Visuals")) {
|
|
783
|
+
findings.push({
|
|
784
|
+
validatorId: this.id,
|
|
785
|
+
severity: "block",
|
|
786
|
+
file: template.file,
|
|
787
|
+
message: `${who}'s Equipment/VisualSet contains a Visuals node — Visuals must be a direct child of Equipment, ` +
|
|
788
|
+
`a sibling of VisualSet (and of Slot and ParentRace), not nested inside it. Nested there the engine ` +
|
|
789
|
+
`finds no worn body map: the item is invisible when equipped on every body type, and examining it ` +
|
|
790
|
+
`falls back to the loot VisualTemplate.`,
|
|
791
|
+
fix: `Move the Visuals node out of VisualSet so it sits directly under Equipment > children, beside Slot and VisualSet.`,
|
|
792
|
+
evidence: this.evidence,
|
|
793
|
+
});
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const slotNode = child(equipment, "Slot");
|
|
797
|
+
const slot = slotNode ? attr(slotNode, "Object") : undefined;
|
|
798
|
+
if (!visuals) {
|
|
799
|
+
if (slot) {
|
|
800
|
+
findings.push({
|
|
801
|
+
validatorId: this.id,
|
|
802
|
+
severity: "block",
|
|
803
|
+
file: template.file,
|
|
804
|
+
message: `${who}'s Equipment declares Slot "${slot}" but has no Visuals child — with no body-type map the ` +
|
|
805
|
+
`worn mesh is invisible on every body type.`,
|
|
806
|
+
fix: `Add a Visuals node directly under Equipment > children with one Object child per body-type MapKey (14 keys) whose MapValue/Object is the VisualBank Resource ID.`,
|
|
807
|
+
evidence: this.evidence,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const mapped = new Set();
|
|
813
|
+
for (const object of children(visuals, "Object")) {
|
|
814
|
+
const key = attr(object, "MapKey");
|
|
815
|
+
if (!key)
|
|
816
|
+
continue;
|
|
817
|
+
mapped.add(key.toLowerCase());
|
|
818
|
+
const mapValue = child(object, "MapValue");
|
|
819
|
+
if (!mapValue || !attr(mapValue, "Object")) {
|
|
820
|
+
const label = BODY_TYPE_KEYS.find((b) => b.key === key.toLowerCase())?.label;
|
|
821
|
+
findings.push({
|
|
822
|
+
validatorId: this.id,
|
|
823
|
+
severity: "warn",
|
|
824
|
+
file: template.file,
|
|
825
|
+
message: `${who}'s Equipment/Visuals maps body type ${key}${label ? ` (${label})` : ""} to an empty MapValue/Object — ` +
|
|
826
|
+
`that body will render the item invisible.`,
|
|
827
|
+
fix: `Set that MapValue's Object attribute to the VisualBank Resource ID of the mesh for this body type.`,
|
|
828
|
+
evidence: this.evidence,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const missing = BODY_TYPE_KEYS.filter((b) => !mapped.has(b.key));
|
|
833
|
+
if (missing.length > 0) {
|
|
834
|
+
findings.push({
|
|
835
|
+
validatorId: this.id,
|
|
836
|
+
severity: "warn",
|
|
837
|
+
file: template.file,
|
|
838
|
+
message: `${who}'s Equipment/Visuals maps ${BODY_TYPE_KEYS.length - missing.length} of the 14 body-type keys; ` +
|
|
839
|
+
`missing ${missing.map((m) => `${m.key} (${m.label})`).join(", ")}. There is no fallback — those bodies ` +
|
|
840
|
+
`will render the item invisible.`,
|
|
841
|
+
fix: `Add an Object child under Visuals for each missing MapKey, pointing MapValue/Object at the matching body's VisualBank Resource ID.`,
|
|
842
|
+
evidence: this.evidence,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return findings;
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
const SCENERY_BASE_MATERIAL = /\/Materials\/Base\//i;
|
|
850
|
+
const wornVisualMaterialDomain = {
|
|
851
|
+
id: "bg3.items.worn-visual-material-domain",
|
|
852
|
+
game: GAME,
|
|
853
|
+
groups: ["items"],
|
|
854
|
+
modTypes: ["armor", "accessory"],
|
|
855
|
+
summary: "A VisualBank Resource with Slot=\"Body\" uses MaterialType 0 and a Materials/Characters/CHAR_BASE* base material, not the weapon/scenery values.",
|
|
856
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
857
|
+
appliesToDescription: "once per project, over every VisualBank Resource whose Slot is \"Body\"",
|
|
858
|
+
needs: { project: true },
|
|
859
|
+
cost: "edit",
|
|
860
|
+
severity: "block",
|
|
861
|
+
evidence: [
|
|
862
|
+
"bg3.items.visualbank.worn-body-resource-needs-character-material-domain",
|
|
863
|
+
"bg3.items.visualbank.material-shader-selection-by-filename",
|
|
864
|
+
],
|
|
865
|
+
async run(ctx) {
|
|
866
|
+
const idx = await collectProjectLsx(ctx);
|
|
867
|
+
const banks = collectBankResources(idx);
|
|
868
|
+
const materialBanks = banks.filter((b) => b.region === "MaterialBank");
|
|
869
|
+
const findings = [];
|
|
870
|
+
for (const visual of banks.filter((b) => b.region === "VisualBank")) {
|
|
871
|
+
if (attr(visual.node, "Slot") !== "Body")
|
|
872
|
+
continue;
|
|
873
|
+
const who = `VisualBank Resource "${visual.name ?? visual.id ?? "?"}" (ID=${visual.id ?? "?"}, Slot="Body")`;
|
|
874
|
+
const materialType = attr(visual.node, "MaterialType");
|
|
875
|
+
if (materialType === "4") {
|
|
876
|
+
findings.push({
|
|
877
|
+
validatorId: this.id,
|
|
878
|
+
severity: "block",
|
|
879
|
+
file: visual.file,
|
|
880
|
+
message: `${who} has MaterialType="4", the weapon/rigid value — a worn body mesh needs MaterialType 0 (character ` +
|
|
881
|
+
`domain); at 4 it gets no skinned shader permutation and is invisible when equipped.`,
|
|
882
|
+
fix: `Set MaterialType="0" on this Resource (and instance a Materials/Characters/CHAR_BASE*.lsf base material).`,
|
|
883
|
+
evidence: this.evidence,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
for (const objects of children(visual.node, "Objects")) {
|
|
887
|
+
const materialId = attr(objects, "MaterialID");
|
|
888
|
+
if (!materialId)
|
|
889
|
+
continue;
|
|
890
|
+
const material = materialBanks.find((m) => m.id === materialId);
|
|
891
|
+
if (!material)
|
|
892
|
+
continue;
|
|
893
|
+
const sourceFile = attr(material.node, "SourceFile");
|
|
894
|
+
if (!sourceFile || !SCENERY_BASE_MATERIAL.test(sourceFile))
|
|
895
|
+
continue;
|
|
896
|
+
findings.push({
|
|
897
|
+
validatorId: this.id,
|
|
898
|
+
severity: "block",
|
|
899
|
+
file: material.file,
|
|
900
|
+
message: `${who}'s Objects/MaterialID "${materialId}" is MaterialBank Resource "${material.name ?? materialId}" whose ` +
|
|
901
|
+
`SourceFile "${sourceFile}" is a generic/scenery base (Materials/Base/) — a worn body mesh on that base ` +
|
|
902
|
+
`has no skinned shader permutation and is invisible when equipped.`,
|
|
903
|
+
fix: `Instance a character-domain base instead: SourceFile Public/Shared/Assets/Materials/Characters/CHAR_BASE.lsf, CHAR_BASE_AlphaTest.lsf or CHAR_BASE_VertCut.lsf (same basecolor/normalmap/physicalmap params).`,
|
|
904
|
+
related: [visual.file],
|
|
905
|
+
evidence: this.evidence,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return findings;
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
const GUID_TYPED_ROOTTEMPLATE_ATTRS = {
|
|
913
|
+
EquipmentTypeID: {
|
|
914
|
+
requirement: 'the engine reads EquipmentTypeID as a GUID (the custom EquipmentType\'s UUID) and vanilla always declares it type="guid"',
|
|
915
|
+
consequence: "the item silently falls back to default one-handed equipment handling (held in one hand, no back-sheath, wrong attack animation)",
|
|
916
|
+
},
|
|
917
|
+
Race: {
|
|
918
|
+
requirement: 'on a character template the engine reads Race as a GUID reference and vanilla declares it type="guid" (on an item template Race is instead a plain int8 flag, which this rule leaves alone)',
|
|
919
|
+
consequence: "the character's race reference is silently dropped and the engine falls back to a default race",
|
|
920
|
+
onlyType: "character",
|
|
921
|
+
},
|
|
922
|
+
Faction: {
|
|
923
|
+
requirement: 'the engine reads Faction as a GUID and vanilla always declares it type="guid"',
|
|
924
|
+
consequence: "the template's faction reference is silently dropped and the engine falls back to a default faction",
|
|
925
|
+
},
|
|
926
|
+
EquipmentRace: {
|
|
927
|
+
requirement: 'the engine reads EquipmentRace as a GUID and vanilla always declares it type="guid"',
|
|
928
|
+
consequence: "the template's equipment-race reference is silently dropped (its race-specific equipment visuals fall back to a default)",
|
|
929
|
+
},
|
|
930
|
+
AiHint: {
|
|
931
|
+
requirement: 'the engine reads AiHint as a GUID and vanilla always declares it type="guid"',
|
|
932
|
+
consequence: "the template's AI hint reference is silently dropped and the engine falls back to default AI behaviour",
|
|
933
|
+
},
|
|
934
|
+
DeathEffect: {
|
|
935
|
+
requirement: 'the engine reads DeathEffect as a GUID and vanilla always declares it type="guid"',
|
|
936
|
+
consequence: "the template's death-effect reference is silently dropped",
|
|
937
|
+
},
|
|
938
|
+
ColorPreset: {
|
|
939
|
+
requirement: 'the engine reads ColorPreset as a GUID and vanilla always declares it type="guid"',
|
|
940
|
+
consequence: "the template's colour-preset reference is silently dropped and the engine falls back to a default",
|
|
941
|
+
},
|
|
942
|
+
};
|
|
943
|
+
const REQUIRED_GUID_TYPE = "guid";
|
|
944
|
+
const roottemplateGuidAttributeType = {
|
|
945
|
+
id: "bg3.items.roottemplate-guid-attribute-type",
|
|
946
|
+
game: GAME,
|
|
947
|
+
groups: ["items"],
|
|
948
|
+
modTypes: ["armor", "weapon", "accessory", "origin", "race"],
|
|
949
|
+
summary: 'A RootTemplate GameObjects attribute the engine reads as a GUID (EquipmentTypeID; Faction/EquipmentRace/AiHint/DeathEffect/ColorPreset; Race on a character) must be declared type="guid"; type="FixedString" is silently ignored.',
|
|
950
|
+
appliesTo: (target) => isProjectTarget(target),
|
|
951
|
+
appliesToDescription: 'once per project, over every GameObjects node\'s known-guid attributes (EquipmentTypeID, Faction, EquipmentRace, AiHint, DeathEffect, ColorPreset, and Race on a character-Type node)',
|
|
952
|
+
needs: { project: true },
|
|
953
|
+
cost: "edit",
|
|
954
|
+
severity: "block",
|
|
955
|
+
evidence: [
|
|
956
|
+
"bg3.items.roottemplates.equipmenttypeid-must-be-guid-typed",
|
|
957
|
+
"bg3.weapons.custom-equipment-type",
|
|
958
|
+
],
|
|
959
|
+
async run(ctx) {
|
|
960
|
+
const idx = await collectProjectLsx(ctx);
|
|
961
|
+
const findings = [];
|
|
962
|
+
for (const ref of nodesOf(idx, "GameObjects")) {
|
|
963
|
+
const identity = attr(ref.node, "Name") ?? attr(ref.node, "MapKey") ?? "?";
|
|
964
|
+
const nodeType = attr(ref.node, "Type");
|
|
965
|
+
for (const [attrId, spec] of Object.entries(GUID_TYPED_ROOTTEMPLATE_ATTRS)) {
|
|
966
|
+
const declared = ref.node.attributes[attrId];
|
|
967
|
+
if (!declared)
|
|
968
|
+
continue;
|
|
969
|
+
if (declared.type === REQUIRED_GUID_TYPE)
|
|
970
|
+
continue;
|
|
971
|
+
if (spec.onlyType && nodeType !== spec.onlyType)
|
|
972
|
+
continue;
|
|
973
|
+
findings.push({
|
|
974
|
+
validatorId: this.id,
|
|
975
|
+
severity: "block",
|
|
976
|
+
file: ref.file,
|
|
977
|
+
message: `RootTemplate GameObjects node "${identity}" declares ${attrId} as type="${declared.type}", but ` +
|
|
978
|
+
`${spec.requirement}. A wrong type here is not an error — the attribute is silently ignored and ` +
|
|
979
|
+
`${spec.consequence}.`,
|
|
980
|
+
fix: `Change ${attrId}'s type="${declared.type}" to type="guid" on this GameObjects node.`,
|
|
981
|
+
evidence: this.evidence,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return findings;
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
export const ITEM_VALIDATORS = [
|
|
989
|
+
statsRootTemplateLink,
|
|
990
|
+
roottemplateVisualLink,
|
|
991
|
+
visualMaterialTextureChain,
|
|
992
|
+
ddsExtensionCase,
|
|
993
|
+
texturebankSrgb,
|
|
994
|
+
treasuretableIPrefix,
|
|
995
|
+
iconMapkeyResolves,
|
|
996
|
+
parenttemplateidTypeMatch,
|
|
997
|
+
armorWeaponFields,
|
|
998
|
+
equipmentVisualsStructure,
|
|
999
|
+
wornVisualMaterialDomain,
|
|
1000
|
+
roottemplateGuidAttributeType,
|
|
1001
|
+
];
|