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,476 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { walk } from "../../../core/fsutil.js";
|
|
4
|
+
import { globToRegExp } from "../../../core/validate/context.js";
|
|
5
|
+
import { scanLooseIdentities, scanPaksIdentities } from "../identity.js";
|
|
6
|
+
import { parseLsx, walkLsxNodes } from "../index/parsers/lsx.js";
|
|
7
|
+
function posix(p) {
|
|
8
|
+
return p.split(path.sep).join("/");
|
|
9
|
+
}
|
|
10
|
+
function lower(p) {
|
|
11
|
+
return posix(p).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
export function ddsAssetStem(absPath) {
|
|
14
|
+
const p = absPath.replace(/\\/g, "/");
|
|
15
|
+
return path.posix.basename(p, path.posix.extname(p));
|
|
16
|
+
}
|
|
17
|
+
function fileTarget(match) {
|
|
18
|
+
return (target) => target.kind === "file" && match(lower(target.path));
|
|
19
|
+
}
|
|
20
|
+
function projectTarget(extra) {
|
|
21
|
+
return (target, project) => target.kind === "project" && (!extra || extra(project));
|
|
22
|
+
}
|
|
23
|
+
function finding(validatorId, severity, file, message, evidence, extra) {
|
|
24
|
+
return { validatorId, severity, file, message, evidence, ...extra };
|
|
25
|
+
}
|
|
26
|
+
async function projectFiles(project, match) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const src of project.sources) {
|
|
29
|
+
if (!src.exists)
|
|
30
|
+
continue;
|
|
31
|
+
const { files } = await walk(src.absPath, { limit: 20000 });
|
|
32
|
+
for (const f of files) {
|
|
33
|
+
const abs = path.join(src.absPath, f);
|
|
34
|
+
if (match(lower(abs)))
|
|
35
|
+
out.push(abs);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function hasScriptExtenderDependency(project) {
|
|
41
|
+
return (project.project.dependencies ?? []).some((d) => /script.?extender/i.test(d.id) || /script.?extender/i.test(d.notes ?? ""));
|
|
42
|
+
}
|
|
43
|
+
const BOM_EXTENSIONS = [".txt", ".lsx", ".xml"];
|
|
44
|
+
const noBom = {
|
|
45
|
+
id: "bg3.history.no-bom",
|
|
46
|
+
game: "baldursgate3",
|
|
47
|
+
groups: ["history"],
|
|
48
|
+
modTypes: ["dialogue"],
|
|
49
|
+
summary: "No UTF-8 BOM in .txt/.lsx/.xml files the game parses — PowerShell's default utf8 encoding silently corrupts them.",
|
|
50
|
+
appliesTo: fileTarget((p) => BOM_EXTENSIONS.includes(path.posix.extname(p)) && path.posix.basename(p) !== "meta.lsx"),
|
|
51
|
+
appliesToDescription: "every .txt/.lsx/.xml file in the project, except meta.lsx (Larian's own Toolkit writes that one with a BOM, and it loads fine)",
|
|
52
|
+
needs: { project: true },
|
|
53
|
+
cost: "edit",
|
|
54
|
+
severity: "block",
|
|
55
|
+
evidence: ["bg3.encoding.utf8-bom-corruption"],
|
|
56
|
+
async run(ctx) {
|
|
57
|
+
if (ctx.target.kind !== "file")
|
|
58
|
+
return [];
|
|
59
|
+
const text = await ctx.readText(ctx.target.path);
|
|
60
|
+
if (text.charCodeAt(0) !== 0xfeff)
|
|
61
|
+
return [];
|
|
62
|
+
return [
|
|
63
|
+
finding(noBom.id, "block", ctx.target.path, `This file begins with a UTF-8 BOM. PowerShell's default 'utf8' encoding writes one; the game's parser for this file type does not tolerate it and the corruption is silent.`, noBom.evidence, { fix: "Re-save without a BOM (Python, or PowerShell's 'utf8NoBOM' / 'ASCII' encodings)." }),
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const iconuvRegionPlacement = {
|
|
68
|
+
id: "bg3.history.iconuv-region-placement",
|
|
69
|
+
game: "baldursgate3",
|
|
70
|
+
groups: ["history"],
|
|
71
|
+
modTypes: ["icon"],
|
|
72
|
+
summary: "IconUV entries sit in a sibling IconUVList region, not nested inside TextureAtlasInfo — nesting yields the blue fallback icon.",
|
|
73
|
+
appliesTo: fileTarget((p) => p.endsWith(".lsx")),
|
|
74
|
+
appliesToDescription: "every .lsx file, checked for an atlas descriptor's TextureAtlasInfo/IconUVList region shape",
|
|
75
|
+
needs: { project: true },
|
|
76
|
+
cost: "edit",
|
|
77
|
+
severity: "block",
|
|
78
|
+
evidence: ["bg3.icons.iconuv-region-nesting-gotcha", "bg3.ui.icons-atlas.uv-coordinates-are-normalized-fractions-not-pixels"],
|
|
79
|
+
async run(ctx) {
|
|
80
|
+
if (ctx.target.kind !== "file")
|
|
81
|
+
return [];
|
|
82
|
+
const text = await ctx.readText(ctx.target.path);
|
|
83
|
+
let doc;
|
|
84
|
+
try {
|
|
85
|
+
doc = parseLsx(text, ctx.target.path);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const atlasInfo = doc.regions.find((r) => r.id === "TextureAtlasInfo");
|
|
91
|
+
if (!atlasInfo)
|
|
92
|
+
return [];
|
|
93
|
+
let nestedCount = 0;
|
|
94
|
+
walkLsxNodes(atlasInfo.root, (node) => {
|
|
95
|
+
if (node.id === "IconUV")
|
|
96
|
+
nestedCount++;
|
|
97
|
+
});
|
|
98
|
+
if (nestedCount === 0)
|
|
99
|
+
return [];
|
|
100
|
+
return [
|
|
101
|
+
finding(iconuvRegionPlacement.id, "block", ctx.target.path, `${nestedCount} IconUV node(s) are nested inside the TextureAtlasInfo region. IconUV must be a sibling region (IconUVList), not nested here — nesting produces the generic blue missing-icon fallback in-game.`, iconuvRegionPlacement.evidence, { fix: "Move the IconUV nodes into their own IconUVList region, as a sibling of TextureAtlasInfo." }),
|
|
102
|
+
];
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const guiMetadataPresent = {
|
|
106
|
+
id: "bg3.history.gui-metadata-present",
|
|
107
|
+
game: "baldursgate3",
|
|
108
|
+
groups: ["history"],
|
|
109
|
+
modTypes: ["icon"],
|
|
110
|
+
summary: "Hand-authored GUI DDS assets under Mods/<Mod>/GUI/Assets/** are registered in GUI/metadata.lsf.",
|
|
111
|
+
appliesTo: projectTarget(),
|
|
112
|
+
appliesToDescription: "the whole project: every DDS under a GUI/Assets/ tree (excluding AssetsLowRes/) against a GUI/metadata.lsx twin",
|
|
113
|
+
needs: { project: true, toolchain: ["divine"] },
|
|
114
|
+
cost: "build",
|
|
115
|
+
severity: "block",
|
|
116
|
+
evidence: ["bg3.icons.class-icon-metadata", "bg3.icons.tooltip-asset-system"],
|
|
117
|
+
async run(ctx) {
|
|
118
|
+
if (ctx.target.kind !== "project")
|
|
119
|
+
return [];
|
|
120
|
+
const ddsFiles = await projectFiles(ctx.project, (p) => p.includes("/gui/assets/") && !p.includes("/assetslowres/") && p.endsWith(".dds"));
|
|
121
|
+
if (ddsFiles.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const metadataFiles = await projectFiles(ctx.project, (p) => path.posix.basename(p) === "metadata.lsx");
|
|
124
|
+
if (metadataFiles.length === 0) {
|
|
125
|
+
return [
|
|
126
|
+
finding(guiMetadataPresent.id, "info", ctx.project.root, `${ddsFiles.length} hand-authored GUI DDS asset(s) under GUI/Assets/ found, but no GUI/metadata.lsx (the divine-converted intermediate of metadata.lsf) exists to check registration against. ` +
|
|
127
|
+
`ValidationContext has no toolchain-invocation hook, so this could not be verified — convert GUI/metadata.lsf to .lsx and re-run.`, ["bg3.toolchain.divine-core-tool"]),
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
const findings = [];
|
|
131
|
+
for (const metaFile of metadataFiles) {
|
|
132
|
+
let metaText;
|
|
133
|
+
try {
|
|
134
|
+
metaText = await ctx.readText(metaFile);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const dds of ddsFiles) {
|
|
140
|
+
const stem = ddsAssetStem(dds);
|
|
141
|
+
const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
142
|
+
const needle = new RegExp(`["/]${escapedStem}\\.png"`, "i");
|
|
143
|
+
if (!needle.test(metaText)) {
|
|
144
|
+
findings.push(finding(guiMetadataPresent.id, "block", dds, `No GUI/metadata.lsf entry registers '${stem}.png' for this DDS asset. Without it the game shows "Missing MetaData" and the icon is blank.`, guiMetadataPresent.evidence, { related: [metaFile] }));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return findings;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const locaNoXmlComments = {
|
|
152
|
+
id: "bg3.history.loca-no-xml-comments",
|
|
153
|
+
game: "baldursgate3",
|
|
154
|
+
groups: ["history"],
|
|
155
|
+
modTypes: ["dialogue"],
|
|
156
|
+
summary: "No <!-- --> comments in a Localization .xml — the loca parser CTDs the game whenever the owning module is ACTIVE.",
|
|
157
|
+
appliesTo: fileTarget((p) => p.endsWith(".xml") && p.includes("/localization/")),
|
|
158
|
+
appliesToDescription: "every Localization/**/*.xml file",
|
|
159
|
+
needs: { project: true },
|
|
160
|
+
cost: "edit",
|
|
161
|
+
severity: "block",
|
|
162
|
+
evidence: ["bg3.crash.xml-comments-loca-parse"],
|
|
163
|
+
async run(ctx) {
|
|
164
|
+
if (ctx.target.kind !== "file")
|
|
165
|
+
return [];
|
|
166
|
+
const text = await ctx.readText(ctx.target.path);
|
|
167
|
+
const idx = text.indexOf("<!--");
|
|
168
|
+
if (idx === -1)
|
|
169
|
+
return [];
|
|
170
|
+
const line = text.slice(0, idx).split("\n").length;
|
|
171
|
+
return [
|
|
172
|
+
finding(locaNoXmlComments.id, "block", ctx.target.path, `This Localization XML contains an XML comment (<!-- -->). The loca parser crashes to desktop whenever the owning module is ACTIVE — module-inactive testing will not catch this.`, locaNoXmlComments.evidence, { line, fix: "Remove the comment; it never loads in-game anyway." }),
|
|
173
|
+
];
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const seConfigShape = {
|
|
177
|
+
id: "bg3.history.se-config-shape",
|
|
178
|
+
game: "baldursgate3",
|
|
179
|
+
groups: ["history"],
|
|
180
|
+
modTypes: ["dialogue"],
|
|
181
|
+
summary: "Mods/<Mod>/ScriptExtender/Config.json exists with a capital C and RequiredVersion, not the DOS2-era RequiredExtensionVersion.",
|
|
182
|
+
appliesTo: (target, project) => target.kind === "file" && lower(target.path).endsWith("/scriptextender/config.json") && hasScriptExtenderDependency(project),
|
|
183
|
+
appliesToDescription: "Mods/<Mod>/ScriptExtender/Config.json (any casing), only when the project declares a Script Extender dependency",
|
|
184
|
+
needs: { project: true },
|
|
185
|
+
cost: "edit",
|
|
186
|
+
severity: "block",
|
|
187
|
+
evidence: ["bg3.se.config-required"],
|
|
188
|
+
async run(ctx) {
|
|
189
|
+
if (ctx.target.kind !== "file")
|
|
190
|
+
return [];
|
|
191
|
+
const findings = [];
|
|
192
|
+
const basename = path.basename(ctx.target.path);
|
|
193
|
+
if (basename !== "Config.json") {
|
|
194
|
+
findings.push(finding(seConfigShape.id, "block", ctx.target.path, `The Script Extender config file is named '${basename}'; it must be exactly 'Config.json' (capital C). Pak-internal paths are case-sensitive — a lowercase 'config.json' is tolerated loose but silently fails once packed.`, seConfigShape.evidence, { fix: "Rename the file to Config.json." }));
|
|
195
|
+
}
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(await ctx.readText(ctx.target.path));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return findings;
|
|
202
|
+
}
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "RequiredExtensionVersion")) {
|
|
204
|
+
findings.push(finding(seConfigShape.id, "block", ctx.target.path, `Config.json uses the DOS2-era 'RequiredExtensionVersion' key. BG3's Script Extender expects 'RequiredVersion' instead.`, seConfigShape.evidence, { fix: 'Rename the key to "RequiredVersion".' }));
|
|
205
|
+
}
|
|
206
|
+
else if (!Object.prototype.hasOwnProperty.call(parsed, "RequiredVersion")) {
|
|
207
|
+
findings.push(finding(seConfigShape.id, "block", ctx.target.path, `Config.json has no 'RequiredVersion' key.`, seConfigShape.evidence, { fix: 'Add "RequiredVersion": <SE major version>.' }));
|
|
208
|
+
}
|
|
209
|
+
const modTable = parsed.ModTable;
|
|
210
|
+
const folder = ctx.project.project.identity?.folder;
|
|
211
|
+
if (typeof modTable === "string" && folder && modTable !== folder) {
|
|
212
|
+
findings.push(finding(seConfigShape.id, "warn", ctx.target.path, `Config.json's ModTable is '${modTable}', which does not match the module folder '${folder}'.`, seConfigShape.evidence));
|
|
213
|
+
}
|
|
214
|
+
return findings;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const staleSameUuidPak = {
|
|
218
|
+
id: "bg3.history.stale-same-uuid-pak",
|
|
219
|
+
game: "baldursgate3",
|
|
220
|
+
groups: ["history"],
|
|
221
|
+
modTypes: ["packaging"],
|
|
222
|
+
summary: "A pak for this project in the profile Mods/ folder is older than the project's own sources — it will silently win and shadow your loose edits.",
|
|
223
|
+
appliesTo: projectTarget(),
|
|
224
|
+
appliesToDescription: "the whole project, when an install is located (skipped otherwise) — compares the profile Mods/<Mod>.pak's mtime against the project's own source files",
|
|
225
|
+
needs: { project: true, install: true },
|
|
226
|
+
cost: "build",
|
|
227
|
+
severity: "block",
|
|
228
|
+
evidence: ["bg3.paks.same-uuid-precedence"],
|
|
229
|
+
async run(ctx) {
|
|
230
|
+
if (ctx.target.kind !== "project" || !ctx.install)
|
|
231
|
+
return [];
|
|
232
|
+
const paksRoot = ctx.surface.modRoots(ctx.install).find((r) => r.id === "paks");
|
|
233
|
+
if (!paksRoot)
|
|
234
|
+
return [];
|
|
235
|
+
const identityUuid = ctx.project.project.identity?.uuid;
|
|
236
|
+
const identityFolder = ctx.project.project.identity?.folder;
|
|
237
|
+
let pakPath;
|
|
238
|
+
let pakLabel;
|
|
239
|
+
let pakMtime;
|
|
240
|
+
if (identityUuid) {
|
|
241
|
+
const { copies } = await scanPaksIdentities(paksRoot.path);
|
|
242
|
+
const own = copies.find((c) => c.uuid?.toLowerCase() === identityUuid.toLowerCase() && (!identityFolder || c.folder === identityFolder));
|
|
243
|
+
if (!own)
|
|
244
|
+
return [];
|
|
245
|
+
pakPath = own.path;
|
|
246
|
+
pakLabel = path.basename(own.path);
|
|
247
|
+
pakMtime = new Date(own.modifiedAt);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const deployTarget = ctx.project.project.deploy?.targets.find((t) => t.rootId === "paks");
|
|
251
|
+
const expectedName = deployTarget ? path.basename(deployTarget.from) : identityFolder ? `${identityFolder}.pak` : undefined;
|
|
252
|
+
if (!expectedName)
|
|
253
|
+
return [];
|
|
254
|
+
let entries;
|
|
255
|
+
try {
|
|
256
|
+
entries = await fs.readdir(paksRoot.path);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const match = entries.find((e) => e.toLowerCase() === expectedName.toLowerCase());
|
|
262
|
+
if (!match)
|
|
263
|
+
return [];
|
|
264
|
+
pakPath = path.join(paksRoot.path, match);
|
|
265
|
+
pakLabel = match;
|
|
266
|
+
try {
|
|
267
|
+
pakMtime = (await fs.stat(pakPath)).mtime;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let newestSource = new Date(0);
|
|
274
|
+
for (const src of ctx.project.sources) {
|
|
275
|
+
if (!src.exists)
|
|
276
|
+
continue;
|
|
277
|
+
const result = await walk(src.absPath, { limit: 20000 });
|
|
278
|
+
if (result.modifiedAt > newestSource)
|
|
279
|
+
newestSource = result.modifiedAt;
|
|
280
|
+
}
|
|
281
|
+
if (newestSource <= pakMtime)
|
|
282
|
+
return [];
|
|
283
|
+
return [
|
|
284
|
+
finding(staleSameUuidPak.id, "block", pakPath, `${pakLabel} in the profile Mods/ folder (modified ${pakMtime.toISOString()}) is older than this project's own sources (newest change ${newestSource.toISOString()}). ` +
|
|
285
|
+
`A packed mod of the same identity wins over a loose/junctioned copy — this pak will silently override your newer edits. Rebuild and redeploy the pak.`, staleSameUuidPak.evidence, { related: [ctx.project.root] }),
|
|
286
|
+
];
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
const duplicateModuleUuid = {
|
|
290
|
+
id: "bg3.history.duplicate-module-uuid",
|
|
291
|
+
game: "baldursgate3",
|
|
292
|
+
groups: ["history"],
|
|
293
|
+
modTypes: ["packaging"],
|
|
294
|
+
summary: "No other installed pak or loose module carries this project's own module UUID under a different path — which copy the game loads is not predictable from outside, so content built into one copy can stop loading with no error.",
|
|
295
|
+
appliesTo: projectTarget((project) => Boolean(project.project.identity?.uuid)),
|
|
296
|
+
appliesToDescription: "the whole project, only when it declares identity.uuid and an install is located — scans every .pak in the profile Mods/ folder and every loose Data/Mods/*/meta.lsx for a copy of the same UUID whose module folder is not this project's own identity.folder (and, when the project declares no identity.folder to tell its own copy apart, only reports when two or more copies share the UUID)",
|
|
297
|
+
needs: { project: true, install: true },
|
|
298
|
+
cost: "build",
|
|
299
|
+
severity: "block",
|
|
300
|
+
evidence: ["bg3.paks.same-uuid-precedence", "observed in game (2026-09-10): the copy the Folder attribute named did not load"],
|
|
301
|
+
async run(ctx) {
|
|
302
|
+
if (ctx.target.kind !== "project" || !ctx.install)
|
|
303
|
+
return [];
|
|
304
|
+
const uuid = ctx.project.project.identity?.uuid;
|
|
305
|
+
if (!uuid)
|
|
306
|
+
return [];
|
|
307
|
+
const ownFolder = ctx.project.project.identity?.folder;
|
|
308
|
+
const target = uuid.toLowerCase();
|
|
309
|
+
const roots = ctx.surface.modRoots(ctx.install);
|
|
310
|
+
const paksRoot = roots.find((r) => r.id === "paks");
|
|
311
|
+
if (!paksRoot)
|
|
312
|
+
return [];
|
|
313
|
+
const looseRoot = roots.find((r) => r.id === "loose");
|
|
314
|
+
const { copies: pakCopies } = await scanPaksIdentities(paksRoot.path);
|
|
315
|
+
const looseCopies = looseRoot ? (await scanLooseIdentities(looseRoot.path)).copies : [];
|
|
316
|
+
const sameUuid = [...pakCopies, ...looseCopies].filter((copy) => copy.uuid?.toLowerCase() === target);
|
|
317
|
+
if (!ownFolder) {
|
|
318
|
+
if (sameUuid.length < 2)
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
const duplicates = ownFolder ? sameUuid.filter((copy) => copy.folder !== ownFolder) : sameUuid;
|
|
322
|
+
if (duplicates.length === 0)
|
|
323
|
+
return [];
|
|
324
|
+
let boundFolder;
|
|
325
|
+
if (ctx.surface.loadOrder) {
|
|
326
|
+
const order = await ctx.surface.loadOrder(ctx.install);
|
|
327
|
+
boundFolder = order.find((e) => e.uuid?.toLowerCase() === target)?.folder;
|
|
328
|
+
}
|
|
329
|
+
return duplicates.map((copy) => {
|
|
330
|
+
const boundNote = boundFolder === undefined
|
|
331
|
+
? ""
|
|
332
|
+
: copy.folder === boundFolder
|
|
333
|
+
? ` modsettings.lsx currently names Folder="${boundFolder}" for this UUID, this copy's folder — the one the mod manager last bound, which is a hint only: the running game may load a different copy (observed in game). Ext.Mod.GetMod("${uuid}").Info.Directory at the Script Extender console says which.`
|
|
334
|
+
: ` modsettings.lsx currently names Folder="${boundFolder}" for this UUID, not this copy's folder — a hint only: the running game may still load this copy (observed in game). Ext.Mod.GetMod("${uuid}").Info.Directory at the Script Extender console says which.`;
|
|
335
|
+
const removalNote = copy.rootId === "loose"
|
|
336
|
+
? `Remove the duplicate: a loose module is registered by its Data/Mods/<Folder> directory, so deleting (or moving aside) the directory this meta.lsx sits in takes it out ` +
|
|
337
|
+
`of consideration. A loose copy is usually left over from an earlier loose-mode deploy or a manual unpack.`
|
|
338
|
+
: `Rename or remove the duplicate: the game only scans files with a .pak extension, so renaming this one to .pak.bak (the same convention already used in the profile folder ` +
|
|
339
|
+
`for retired copies) or deleting it takes it out of consideration. If it came from a mod.io subscription, unsubscribing and deleting it is safe — mod.io will re-download it ` +
|
|
340
|
+
`if you resubscribe.`;
|
|
341
|
+
return finding(duplicateModuleUuid.id, "block", copy.path, `${copy.path} (module folder "${copy.folder}", modified ${copy.modifiedAt}) carries this project's module UUID (${uuid}) but is a different copy from this project's own ` +
|
|
342
|
+
`deploy target${ownFolder ? ` ("${ownFolder}")` : ""}. BG3 identifies a mod by this UUID, not by filename, and which of several copies it loads is not predictable ` +
|
|
343
|
+
`from outside (observed in game), which is how content built into one copy stops loading with no error anywhere. ` +
|
|
344
|
+
`${removalNote}${boundNote}`, duplicateModuleUuid.evidence, { related: [ctx.project.root] });
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
349
|
+
const externalGuidLoadOrder = {
|
|
350
|
+
id: "bg3.history.external-guid-load-order",
|
|
351
|
+
game: "baldursgate3",
|
|
352
|
+
groups: ["history"],
|
|
353
|
+
modTypes: ["dialogue", "origin", "packaging"],
|
|
354
|
+
summary: "A GUID referenced by the mod that resolves in neither the mod nor the vanilla index is an external dependency that must be declared, or a typo that must be fixed.",
|
|
355
|
+
appliesTo: projectTarget(),
|
|
356
|
+
appliesToDescription: "the whole project: every attribute value across .lsx files that looks like a GUID and is not a definition (a 'UUID' attribute, or a bank Resource node's 'ID'), excluding Progressions/RootTemplates/ClassDescriptions and the TableUUID/ParentGuid/VisualTemplate/RootTemplate/ParentTemplateId attributes, which the G2 (items) and G6 (progression) rule groups already own with their own, more specific injection-vs-typo narrowing",
|
|
357
|
+
needs: { project: true, index: true },
|
|
358
|
+
cost: "edit",
|
|
359
|
+
severity: "block",
|
|
360
|
+
evidence: ["bg3.loadorder.external-guid-dependencies"],
|
|
361
|
+
async run(ctx) {
|
|
362
|
+
if (ctx.target.kind !== "project")
|
|
363
|
+
return [];
|
|
364
|
+
const OWNED_ELSEWHERE_PATH = /\/(progressions|roottemplates|classdescriptions)\//;
|
|
365
|
+
const OWNED_ELSEWHERE_ATTR = new Set(["TableUUID", "ParentGuid", "VisualTemplate", "RootTemplate", "ParentTemplateId"]);
|
|
366
|
+
const lsxFiles = await projectFiles(ctx.project, (p) => p.endsWith(".lsx"));
|
|
367
|
+
const defined = new Set();
|
|
368
|
+
const references = [];
|
|
369
|
+
for (const f of lsxFiles) {
|
|
370
|
+
let doc;
|
|
371
|
+
try {
|
|
372
|
+
doc = parseLsx(await ctx.readText(f), f);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const skipReferences = OWNED_ELSEWHERE_PATH.test(lower(f));
|
|
378
|
+
for (const region of doc.regions) {
|
|
379
|
+
walkLsxNodes(region.root, (node) => {
|
|
380
|
+
for (const [attrId, attr] of Object.entries(node.attributes)) {
|
|
381
|
+
if (!UUID_RE.test(attr.value))
|
|
382
|
+
continue;
|
|
383
|
+
if (attrId === "UUID" || (attrId === "ID" && node.id === "Resource")) {
|
|
384
|
+
defined.add(attr.value.toLowerCase());
|
|
385
|
+
}
|
|
386
|
+
else if (!skipReferences && !OWNED_ELSEWHERE_ATTR.has(attrId)) {
|
|
387
|
+
references.push({ uuid: attr.value.toLowerCase(), file: f, attr: attrId });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (references.length === 0)
|
|
394
|
+
return [];
|
|
395
|
+
const hasModDependency = (ctx.project.project.dependencies ?? []).some((d) => d.kind === "mod");
|
|
396
|
+
const findings = [];
|
|
397
|
+
const reported = new Set();
|
|
398
|
+
for (const ref of references) {
|
|
399
|
+
const key = `${ref.file}:${ref.attr}:${ref.uuid}`;
|
|
400
|
+
if (reported.has(key))
|
|
401
|
+
continue;
|
|
402
|
+
if (defined.has(ref.uuid))
|
|
403
|
+
continue;
|
|
404
|
+
const vanilla = await ctx.lookup("guid", { uuid: ref.uuid });
|
|
405
|
+
if (vanilla.count > 0)
|
|
406
|
+
continue;
|
|
407
|
+
reported.add(key);
|
|
408
|
+
findings.push(finding(externalGuidLoadOrder.id, "block", ref.file, `${ref.attr} references ${ref.uuid}, which is defined neither in this project nor in the vanilla index. ` +
|
|
409
|
+
`Either this is an external mod's GUID that must be declared as a "mod" dependency in modwright.json${hasModDependency ? " (one is already declared, but not one that defines this GUID)" : ""}, or it is a typo — both readings point at the same finding.`, externalGuidLoadOrder.evidence));
|
|
410
|
+
}
|
|
411
|
+
return findings;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
function divineLsxToLsfGlobs(project) {
|
|
415
|
+
const steps = project.project.build?.steps ?? [];
|
|
416
|
+
const out = [];
|
|
417
|
+
for (const s of steps) {
|
|
418
|
+
if (typeof s === "string")
|
|
419
|
+
continue;
|
|
420
|
+
if (s.step !== "divine-lsx-to-lsf")
|
|
421
|
+
continue;
|
|
422
|
+
const glob = s.glob ?? "**/*.lsx";
|
|
423
|
+
out.push(globToRegExp(glob));
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
const roottemplateLsxWithoutLsf = {
|
|
428
|
+
id: "bg3.history.roottemplate-lsx-without-lsf",
|
|
429
|
+
game: "baldursgate3",
|
|
430
|
+
groups: ["history"],
|
|
431
|
+
modTypes: ["packaging"],
|
|
432
|
+
summary: "Every Public/*/RootTemplates/*.lsx either has a divine-lsx-to-lsf build step covering it or an .lsf twin beside it — a loose .lsx there is never loaded by the game.",
|
|
433
|
+
appliesTo: projectTarget(),
|
|
434
|
+
appliesToDescription: "the whole project: every Public/*/RootTemplates/*.lsx among its sources, checked against build.steps' own divine-lsx-to-lsf glob(s) and for an .lsf file of the same name beside it",
|
|
435
|
+
needs: { project: true },
|
|
436
|
+
cost: "edit",
|
|
437
|
+
severity: "block",
|
|
438
|
+
evidence: [
|
|
439
|
+
"bg3.items.roottemplates.loose-lsx-in-roottemplates-is-not-loaded-a-separate-lsf-is",
|
|
440
|
+
"observed in game (2026-09-10): a loose .lsx MapKey resolved to nothing; the same node as .lsf loaded",
|
|
441
|
+
],
|
|
442
|
+
async run(ctx) {
|
|
443
|
+
if (ctx.target.kind !== "project")
|
|
444
|
+
return [];
|
|
445
|
+
const lsxFiles = await projectFiles(ctx.project, (p) => p.includes("/roottemplates/") && p.endsWith(".lsx"));
|
|
446
|
+
if (lsxFiles.length === 0)
|
|
447
|
+
return [];
|
|
448
|
+
const conversionGlobs = divineLsxToLsfGlobs(ctx.project);
|
|
449
|
+
const findings = [];
|
|
450
|
+
for (const abs of lsxFiles) {
|
|
451
|
+
const relPosix = posix(path.relative(ctx.project.root, abs));
|
|
452
|
+
if (conversionGlobs.some((re) => re.test(relPosix)))
|
|
453
|
+
continue;
|
|
454
|
+
const lsfTwin = abs.replace(/\.lsx$/i, ".lsf");
|
|
455
|
+
if (await ctx.exists(lsfTwin))
|
|
456
|
+
continue;
|
|
457
|
+
findings.push(finding(roottemplateLsxWithoutLsf.id, "block", abs, `Baldur's Gate 3 does not load a loose .lsx under RootTemplates — Ext.Template.GetTemplate and Osi.CreateAt both return nothing for its MapKey; the same node reaches the ` +
|
|
458
|
+
`engine only as an .lsf. Fix it either way: add a "divine-lsx-to-lsf" build step whose glob covers ${relPosix} (so the build converts it before packing), or convert this file ` +
|
|
459
|
+
`by hand (divine -a convert-resource) into an .lsf of the same name beside it.`, roottemplateLsxWithoutLsf.evidence, {
|
|
460
|
+
fix: `Add a "divine-lsx-to-lsf" build step covering ${relPosix}, or convert it to an .lsf twin (${path.basename(lsfTwin)}) beside it.`,
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
return findings;
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
export const FILE_VALIDATORS = [
|
|
467
|
+
noBom,
|
|
468
|
+
iconuvRegionPlacement,
|
|
469
|
+
guiMetadataPresent,
|
|
470
|
+
locaNoXmlComments,
|
|
471
|
+
seConfigShape,
|
|
472
|
+
staleSameUuidPak,
|
|
473
|
+
duplicateModuleUuid,
|
|
474
|
+
externalGuidLoadOrder,
|
|
475
|
+
roottemplateLsxWithoutLsf,
|
|
476
|
+
];
|