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,240 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { documents, localAppData, pathExists, readTextIfExists } from "../../core/fsutil.js";
|
|
3
|
+
import { readRegistryValue } from "../../core/registry.js";
|
|
4
|
+
import { findInEpic } from "../../core/epic.js";
|
|
5
|
+
import { findInSteam } from "../../core/steam.js";
|
|
6
|
+
import { dedupeByRoot, logs, roots } from "../../core/surfaceutil.js";
|
|
7
|
+
import { SIGNATURES } from "./logs.js";
|
|
8
|
+
import { STOREFRONT_FOLDER, readLoadOrder, resolvePluginsTxt, storefrontOfSync, userDataDirs } from "./loadorder.js";
|
|
9
|
+
import { pluginExtension, readPluginHeader } from "./plugins.js";
|
|
10
|
+
import { PLUGIN_VALIDATORS } from "./validators/plugins.js";
|
|
11
|
+
import { RECORD_VALIDATORS } from "./validators/records.js";
|
|
12
|
+
import { ARCHIVE_RECORD_VALIDATORS } from "./validators/archive-records.js";
|
|
13
|
+
import { ARCHIVE_VALIDATORS } from "./validators/archives.js";
|
|
14
|
+
import { skyrimseCompat } from "./compat.js";
|
|
15
|
+
export const SKYRIM_EPIC_APP_NAMES = ["ac82db5035584c7f8a2c548d98c86b2c", "5d600e4f59974aeba0259c7734134e27"];
|
|
16
|
+
export const skyrimse = {
|
|
17
|
+
id: "skyrimse",
|
|
18
|
+
name: "Skyrim Special Edition",
|
|
19
|
+
aliases: ["skyrim", "skyrimspecialedition", "sse", "skse"],
|
|
20
|
+
summary: "Creation Engine. Plugins (.esp/.esm/.esl) and archives (.bsa) in Data/, " +
|
|
21
|
+
"with load order in Plugins.txt and native extensions via SKSE64.",
|
|
22
|
+
async detect() {
|
|
23
|
+
const installs = [];
|
|
24
|
+
const override = process.env.MODWRIGHT_SKYRIMSE_ROOT;
|
|
25
|
+
if (override && (await pathExists(override))) {
|
|
26
|
+
installs.push({ gameId: "skyrimse", root: override, source: "env" });
|
|
27
|
+
}
|
|
28
|
+
for (const root of await findInSteam(["Skyrim Special Edition"])) {
|
|
29
|
+
if (await pathExists(path.join(root, "SkyrimSE.exe"))) {
|
|
30
|
+
installs.push({ gameId: "skyrimse", root, source: "steam" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const { keys, value, source } of REGISTRY_INSTALL_KEYS) {
|
|
34
|
+
const hit = await readRegistryValue(keys, value);
|
|
35
|
+
if (hit && (await pathExists(path.join(hit.value, "SkyrimSE.exe")))) {
|
|
36
|
+
installs.push({ gameId: "skyrimse", root: hit.value, source });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const root of await findInEpic(SKYRIM_EPIC_APP_NAMES)) {
|
|
40
|
+
if (await pathExists(path.join(root, "SkyrimSE.exe"))) {
|
|
41
|
+
installs.push({ gameId: "skyrimse", root, source: "epic" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return dedupeByRoot(installs);
|
|
45
|
+
},
|
|
46
|
+
modRoots(install) {
|
|
47
|
+
const data = path.join(install.root, "Data");
|
|
48
|
+
return roots([
|
|
49
|
+
{
|
|
50
|
+
id: "plugins",
|
|
51
|
+
label: "Plugins",
|
|
52
|
+
path: data,
|
|
53
|
+
layout: "file",
|
|
54
|
+
extensions: [".esp", ".esm", ".esl"],
|
|
55
|
+
ordered: true,
|
|
56
|
+
conflictScope: "opaque",
|
|
57
|
+
notes: "Load order comes from Plugins.txt, not the filesystem. Whether a plugin is light " +
|
|
58
|
+
"(ESL) is a header flag (0x200), which the .esl extension forces; light plugins " +
|
|
59
|
+
"are active only when Plugins.txt marks them, load at their listed position, and " +
|
|
60
|
+
"take their FormIDs from the FE index space, which costs one full slot collectively " +
|
|
61
|
+
"(254 full plugins once any light one is active; 4096 light).",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "archives",
|
|
65
|
+
label: "BSA archives",
|
|
66
|
+
path: data,
|
|
67
|
+
layout: "file",
|
|
68
|
+
extensions: [".bsa"],
|
|
69
|
+
conflictScope: "opaque",
|
|
70
|
+
notes: "A .bsa loads if a plugin of the same base name is active, or if it is listed under " +
|
|
71
|
+
"[Archive] in Skyrim.ini as SResourceArchiveList or SResourceArchiveList2 (the second " +
|
|
72
|
+
"key holds the tail of a list longer than 255 characters). Skyrim SE archives are " +
|
|
73
|
+
"version 105 (LZ4); a Legendary Edition version-104 archive is a different format.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "skse",
|
|
77
|
+
label: "SKSE plugins",
|
|
78
|
+
path: path.join(data, "SKSE", "Plugins"),
|
|
79
|
+
layout: "file",
|
|
80
|
+
extensions: [".dll"],
|
|
81
|
+
conflictScope: "isolated",
|
|
82
|
+
notes: "Native SKSE64 plugins, version-locked to the game runtime unless built against the " +
|
|
83
|
+
"Address Library, whose versionlib-<runtime>.bin files also live here and are data, " +
|
|
84
|
+
"not plugins.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "loosefiles",
|
|
88
|
+
label: "Loose files (meshes/textures/scripts)",
|
|
89
|
+
path: data,
|
|
90
|
+
layout: "flat",
|
|
91
|
+
conflictScope: "merged",
|
|
92
|
+
notes: "Loose files always beat .bsa content, regardless of plugin order. Plugins and " +
|
|
93
|
+
"archives are the other roots' business, not loose-file conflicts.",
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
},
|
|
97
|
+
async readManifest(modPath, root) {
|
|
98
|
+
if (root.id !== "plugins" || pluginExtension(modPath) === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const header = await readPluginHeader(modPath);
|
|
102
|
+
return {
|
|
103
|
+
format: "tes4-header",
|
|
104
|
+
...(header.author ? { author: header.author } : {}),
|
|
105
|
+
...(header.description ? { description: header.description } : {}),
|
|
106
|
+
dependencies: header.masters,
|
|
107
|
+
raw: {
|
|
108
|
+
flags: header.flags,
|
|
109
|
+
isMaster: header.isMaster,
|
|
110
|
+
isLight: header.isLight,
|
|
111
|
+
isLocalized: header.isLocalized,
|
|
112
|
+
formVersion: header.formVersion,
|
|
113
|
+
headerVersion: header.headerVersion,
|
|
114
|
+
recordCount: header.recordCount,
|
|
115
|
+
nextObjectId: header.nextObjectId,
|
|
116
|
+
masters: header.masters,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async loadOrder(install) {
|
|
125
|
+
return (await readLoadOrder(install)).entries;
|
|
126
|
+
},
|
|
127
|
+
loadOrderFile(install) {
|
|
128
|
+
return localAppData(STOREFRONT_FOLDER[storefrontOfSync(install)], "Plugins.txt");
|
|
129
|
+
},
|
|
130
|
+
logs(_install) {
|
|
131
|
+
const folders = Object.values(STOREFRONT_FOLDER);
|
|
132
|
+
const myGames = (folder, ...segments) => documents("My Games", folder, ...segments);
|
|
133
|
+
return logs([
|
|
134
|
+
{
|
|
135
|
+
id: "skse-loader",
|
|
136
|
+
label: "SKSE64 loader log",
|
|
137
|
+
candidates: folders.map((f) => myGames(f, "SKSE", "skse64_loader.log")),
|
|
138
|
+
rootId: "skse",
|
|
139
|
+
notes: "Written by skse64_loader.exe, so it exists even when the game fails to start. " +
|
|
140
|
+
"Names the SKSE build and the runtime it launched. No timestamps, no level prefixes.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "skse",
|
|
144
|
+
label: "SKSE64 plugin log",
|
|
145
|
+
candidates: folders.map((f) => myGames(f, "SKSE", "skse64.log")),
|
|
146
|
+
rootId: "skse",
|
|
147
|
+
notes: "The plugin roll-call: one 'plugin <dll> (...) loaded correctly (handle n)' per " +
|
|
148
|
+
"plugin, or the reason it was disabled. No timestamps, no level prefixes.",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "papyrus",
|
|
152
|
+
label: "Papyrus script log",
|
|
153
|
+
candidates: folders.map((f) => myGames(f, "Logs", "Script", "Papyrus.0.log")),
|
|
154
|
+
notes: "Only written when [Papyrus] bEnableLogging=1 in Skyrim.ini. Papyrus.0 is the " +
|
|
155
|
+
"newest of four; a new session replaces it. Noisy by design, and most lines " +
|
|
156
|
+
"describe some mod's scripts, not the engine.",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "crash",
|
|
160
|
+
label: "Crash log (CrashLoggerSSE or Trainwreck)",
|
|
161
|
+
candidates: folders.map((f) => myGames(f, "SKSE", "crash-*.log")),
|
|
162
|
+
rootId: "skse",
|
|
163
|
+
notes: "CrashLoggerSSE writes crash-<timestamp>.log here by default; Trainwreck's directory " +
|
|
164
|
+
"is configurable. The plugin-section header (SKSE PLUGINS: vs SCRIPT EXTENDER " +
|
|
165
|
+
"PLUGINS:) tells the two formats apart.",
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
},
|
|
169
|
+
async explainMissing(install) {
|
|
170
|
+
const reasons = {};
|
|
171
|
+
const dirs = await userDataDirs(install);
|
|
172
|
+
const ini = await readTextIfExists(path.join(dirs.myGames, "Skyrim.ini"));
|
|
173
|
+
if (ini === undefined) {
|
|
174
|
+
reasons.papyrus = `${path.join(dirs.myGames, "Skyrim.ini")} was not found, so whether [Papyrus] bEnableLogging is on cannot be read; the game writes Skyrim.ini on first launch.`;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const enabled = papyrusLoggingEnabled(ini);
|
|
178
|
+
if (enabled === false || enabled === undefined) {
|
|
179
|
+
reasons.papyrus =
|
|
180
|
+
`[Papyrus] bEnableLogging is ${enabled === undefined ? "not set (default 0)" : "0"} in ${path.join(dirs.myGames, "Skyrim.ini")}, ` +
|
|
181
|
+
"so the game never writes Papyrus.0.log; set it to 1 to get one.";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const loaderLog = path.join(dirs.myGames, "SKSE", "skse64_loader.log");
|
|
185
|
+
const pluginLog = path.join(dirs.myGames, "SKSE", "skse64.log");
|
|
186
|
+
if ((await pathExists(loaderLog)) && !(await pathExists(pluginLog))) {
|
|
187
|
+
reasons.skse = `${loaderLog} exists but ${pluginLog} does not: the loader ran, the game did not reach plugin loading, or the game was launched without skse64_loader.exe.`;
|
|
188
|
+
}
|
|
189
|
+
else if (!(await pathExists(loaderLog))) {
|
|
190
|
+
reasons["skse-loader"] = `${loaderLog} was not found: SKSE64 has not been run for this install (launch through skse64_loader.exe), or the install is a different storefront's (${dirs.storefront} was assumed).`;
|
|
191
|
+
reasons.skse = reasons["skse-loader"];
|
|
192
|
+
}
|
|
193
|
+
reasons.crash = "A crash log exists only after a crash, and only with CrashLoggerSSE or Trainwreck installed.";
|
|
194
|
+
return reasons;
|
|
195
|
+
},
|
|
196
|
+
logSignatures: SIGNATURES,
|
|
197
|
+
validators: [...PLUGIN_VALIDATORS, ...RECORD_VALIDATORS, ...ARCHIVE_VALIDATORS, ...ARCHIVE_RECORD_VALIDATORS],
|
|
198
|
+
compat: skyrimseCompat,
|
|
199
|
+
processNames: ["SkyrimSE.exe"],
|
|
200
|
+
guidance: "Plugin order is authoritative and file-level conflict detection under Data/ will be " +
|
|
201
|
+
"noisy — most real conflicts here are record-level between plugins, which needs xEdit " +
|
|
202
|
+
"rather than a directory diff. Treat ModWright's Data/ conflicts as a hint about loose " +
|
|
203
|
+
"files, and read the load order for anything about ordering. Only Steam installs and " +
|
|
204
|
+
"MODWRIGHT_SKYRIMSE_ROOT are auto-detected, plus the game's own and GOG's registry keys; " +
|
|
205
|
+
"Epic and Microsoft Store installs are not found without the override, and the user-data " +
|
|
206
|
+
"folders differ per storefront (Skyrim Special Edition GOG / EPIC / MS under LocalAppData " +
|
|
207
|
+
"and My Games). Under Mod Organizer 2 the load " +
|
|
208
|
+
"order read is the active profile's plugins.txt, not the LocalAppData copy the game sees " +
|
|
209
|
+
"only inside MO2. None of this has yet been checked against a running game.",
|
|
210
|
+
};
|
|
211
|
+
const REGISTRY_INSTALL_KEYS = [
|
|
212
|
+
{
|
|
213
|
+
keys: ["HKLM\\SOFTWARE\\Bethesda Softworks\\Skyrim Special Edition", "HKLM\\SOFTWARE\\WOW6432Node\\Bethesda Softworks\\Skyrim Special Edition"],
|
|
214
|
+
value: "Installed Path",
|
|
215
|
+
source: "config",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
keys: ["HKLM\\SOFTWARE\\GOG.com\\Games\\1162721350", "HKLM\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\1162721350", "HKLM\\SOFTWARE\\GOG.com\\Games\\1711230643", "HKLM\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\1711230643"],
|
|
219
|
+
value: "path",
|
|
220
|
+
source: "gog",
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
export function papyrusLoggingEnabled(iniText) {
|
|
224
|
+
let section = "";
|
|
225
|
+
for (const raw of iniText.split(/\r?\n/)) {
|
|
226
|
+
const line = raw.trim();
|
|
227
|
+
const sec = /^\[(.+)\]$/.exec(line);
|
|
228
|
+
if (sec) {
|
|
229
|
+
section = sec[1].trim().toLowerCase();
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (section !== "papyrus")
|
|
233
|
+
continue;
|
|
234
|
+
const m = /^benablelogging\s*=\s*(\d+)/i.exec(line);
|
|
235
|
+
if (m)
|
|
236
|
+
return m[1] !== "0";
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
export { resolvePluginsTxt };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { BsaHeaderError, readBsaTable, tesHash } from "../archives.js";
|
|
3
|
+
const EVIDENCE = [
|
|
4
|
+
"sse.archives.bsa.folder-record-is-24-bytes-in-v105",
|
|
5
|
+
"sse.archives.bsa.file-record-blocks-and-name-block",
|
|
6
|
+
"sse.archives.bsa.name-hash-algorithm",
|
|
7
|
+
];
|
|
8
|
+
const APPLIES = "every .bsa file under a project source";
|
|
9
|
+
const MAX_PER_FILE = 20;
|
|
10
|
+
function isWithin(parent, child) {
|
|
11
|
+
const rel = path.relative(parent, child);
|
|
12
|
+
return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
|
|
13
|
+
}
|
|
14
|
+
function isBsaUnderSource(target, project) {
|
|
15
|
+
if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ".bsa")
|
|
16
|
+
return false;
|
|
17
|
+
return project.sources.some((s) => s.exists && isWithin(s.absPath, target.path));
|
|
18
|
+
}
|
|
19
|
+
async function tableOrFinding(id, file) {
|
|
20
|
+
try {
|
|
21
|
+
return await readBsaTable(file);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const reason = err instanceof BsaHeaderError ? err.reason : "unreadable";
|
|
25
|
+
return {
|
|
26
|
+
validatorId: id,
|
|
27
|
+
severity: "block",
|
|
28
|
+
file,
|
|
29
|
+
message: `${path.basename(file)}'s folder and file tables cannot be read (${reason}): ${err.message}`,
|
|
30
|
+
fix: "Repack the archive with BSArch or the Creation Kit's archive tool.",
|
|
31
|
+
evidence: EVIDENCE,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const isFinding = (v) => v.validatorId !== undefined;
|
|
36
|
+
function cap(findings, id, file) {
|
|
37
|
+
if (findings.length <= MAX_PER_FILE)
|
|
38
|
+
return findings;
|
|
39
|
+
return [...findings.slice(0, MAX_PER_FILE), { validatorId: id, severity: "info", file, message: `${findings.length - MAX_PER_FILE} more finding(s) of this kind in the archive not listed.`, evidence: EVIDENCE }];
|
|
40
|
+
}
|
|
41
|
+
const hex = (h) => `0x${h.toString(16).padStart(16, "0")}`;
|
|
42
|
+
const archiveRecords = {
|
|
43
|
+
id: "sse.archives.records",
|
|
44
|
+
game: "skyrimse",
|
|
45
|
+
groups: ["plugins"],
|
|
46
|
+
modTypes: ["packaging"],
|
|
47
|
+
summary: "A .bsa's folder records, file record blocks and file name block are consistent with its header (counts, name-block lengths, block offsets) and every file's data lies inside the archive.",
|
|
48
|
+
appliesTo: isBsaUnderSource,
|
|
49
|
+
appliesToDescription: APPLIES,
|
|
50
|
+
needs: { project: true },
|
|
51
|
+
cost: "build",
|
|
52
|
+
severity: "block",
|
|
53
|
+
evidence: EVIDENCE,
|
|
54
|
+
async run(ctx) {
|
|
55
|
+
if (ctx.target.kind !== "file")
|
|
56
|
+
return [];
|
|
57
|
+
const read = await tableOrFinding(this.id, ctx.target.path);
|
|
58
|
+
if (isFinding(read))
|
|
59
|
+
return [read];
|
|
60
|
+
const file = ctx.target.path;
|
|
61
|
+
const name = path.basename(file);
|
|
62
|
+
const findings = read.table.problems.map((p) => ({ validatorId: this.id, severity: "block", file, message: `${name}: ${p}`, fix: "Repack the archive; a table the game cannot walk loads nothing from it.", evidence: EVIDENCE }));
|
|
63
|
+
for (const f of read.table.files) {
|
|
64
|
+
const end = f.offset + f.size;
|
|
65
|
+
if (f.offset < read.table.tableLength || end > read.fileSize) {
|
|
66
|
+
const label = f.name !== undefined ? `"${f.name}"` : `file record ${f.index}`;
|
|
67
|
+
findings.push({ validatorId: this.id, severity: "block", file, message: `${name}: ${label} in folder ${read.table.folders[f.folder]?.name ?? f.folder} has its data at bytes ${f.offset} to ${end}, ${f.offset < read.table.tableLength ? "inside the tables" : `past the end of the ${read.fileSize}-byte archive`}.`, fix: "Repack the archive; the record cannot be read.", evidence: EVIDENCE });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return cap(findings, this.id, file);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const archiveHashOrder = {
|
|
74
|
+
id: "sse.archives.hash-order",
|
|
75
|
+
game: "skyrimse",
|
|
76
|
+
groups: ["plugins"],
|
|
77
|
+
modTypes: ["packaging"],
|
|
78
|
+
summary: "A .bsa's folder records, and the file records within each folder, are sorted by their 64-bit hash (UESP: 'Files and Dirs order'), and no two neighbours carry the same hash.",
|
|
79
|
+
appliesTo: isBsaUnderSource,
|
|
80
|
+
appliesToDescription: APPLIES,
|
|
81
|
+
needs: { project: true },
|
|
82
|
+
cost: "build",
|
|
83
|
+
severity: "warn",
|
|
84
|
+
evidence: EVIDENCE,
|
|
85
|
+
async run(ctx) {
|
|
86
|
+
if (ctx.target.kind !== "file")
|
|
87
|
+
return [];
|
|
88
|
+
const read = await tableOrFinding(this.id, ctx.target.path);
|
|
89
|
+
if (isFinding(read))
|
|
90
|
+
return [];
|
|
91
|
+
const file = ctx.target.path;
|
|
92
|
+
const name = path.basename(file);
|
|
93
|
+
const findings = [];
|
|
94
|
+
const { folders, files } = read.table;
|
|
95
|
+
for (let i = 1; i < folders.length; i++) {
|
|
96
|
+
const a = folders[i - 1];
|
|
97
|
+
const b = folders[i];
|
|
98
|
+
if (b.hash > a.hash)
|
|
99
|
+
continue;
|
|
100
|
+
const at = `folder record ${i} (${b.name ?? hex(b.hash)})`;
|
|
101
|
+
const before = `record ${i - 1} (${a.name ?? hex(a.hash)})`;
|
|
102
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: b.hash === a.hash ? `${name}: ${at} and ${before} carry the same hash ${hex(b.hash)}; folders are found by hash, so one of the two cannot be reached.` : `${name}: ${at} is not after ${before} in hash order; UESP says folders must be sorted by hash, and a lookup that assumes the order can miss the folder.`, fix: "Repack with BSArch or the Creation Kit, which write the records in hash order.", evidence: EVIDENCE });
|
|
103
|
+
}
|
|
104
|
+
for (let i = 1; i < files.length; i++) {
|
|
105
|
+
const a = files[i - 1];
|
|
106
|
+
const b = files[i];
|
|
107
|
+
if (a.folder !== b.folder)
|
|
108
|
+
continue;
|
|
109
|
+
if (b.hash > a.hash)
|
|
110
|
+
continue;
|
|
111
|
+
const where = `within folder ${folders[b.folder]?.name ?? b.folder}`;
|
|
112
|
+
const at = `file record ${b.index} (${b.name ?? hex(b.hash)})`;
|
|
113
|
+
const before = `record ${a.index} (${a.name ?? hex(a.hash)})`;
|
|
114
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: b.hash === a.hash ? `${name}: ${at} and ${before} carry the same hash ${hex(b.hash)} ${where}; files are found by hash, so one of the two cannot be reached.` : `${name}: ${at} is not after ${before} in hash order ${where}; UESP says files must be sorted by hash.`, fix: "Repack with BSArch or the Creation Kit.", evidence: EVIDENCE });
|
|
115
|
+
}
|
|
116
|
+
return cap(findings, this.id, file);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const archiveHashesMatch = {
|
|
120
|
+
id: "sse.archives.hashes-match-names",
|
|
121
|
+
game: "skyrimse",
|
|
122
|
+
groups: ["plugins"],
|
|
123
|
+
modTypes: ["packaging"],
|
|
124
|
+
summary: "Every folder and file record's hash is the Tes4/Tes5 hash of the name the archive carries for it (lower case, backslashes, the extension hashed separately); a record whose hash is not its name's cannot be found by name.",
|
|
125
|
+
appliesTo: isBsaUnderSource,
|
|
126
|
+
appliesToDescription: APPLIES,
|
|
127
|
+
needs: { project: true },
|
|
128
|
+
cost: "build",
|
|
129
|
+
severity: "block",
|
|
130
|
+
evidence: EVIDENCE,
|
|
131
|
+
async run(ctx) {
|
|
132
|
+
if (ctx.target.kind !== "file")
|
|
133
|
+
return [];
|
|
134
|
+
const read = await tableOrFinding(this.id, ctx.target.path);
|
|
135
|
+
if (isFinding(read))
|
|
136
|
+
return [];
|
|
137
|
+
const file = ctx.target.path;
|
|
138
|
+
const name = path.basename(file);
|
|
139
|
+
const { header, folders, files } = read.table;
|
|
140
|
+
if (!header.includesDirectoryNames && !header.includesFileNames) {
|
|
141
|
+
return [{ validatorId: this.id, severity: "info", file, message: `${name} carries no folder or file names (flags 0x1 and 0x2 unset), so its hashes cannot be checked against names; sse.archives.name-flags reports the flags themselves.`, evidence: EVIDENCE }];
|
|
142
|
+
}
|
|
143
|
+
const findings = [];
|
|
144
|
+
if (header.includesDirectoryNames) {
|
|
145
|
+
for (const folder of folders) {
|
|
146
|
+
if (folder.name === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
const expected = tesHash(folder.name, "folder");
|
|
149
|
+
if (expected !== folder.hash)
|
|
150
|
+
findings.push({ validatorId: this.id, severity: "block", file, message: `${name}: folder "${folder.name}" is recorded with hash ${hex(folder.hash)}, but its name hashes to ${hex(expected)}; the game looks folders up by hash, so nothing in it can be found by name.`, fix: "Repack the archive; a tool that wrote the hash from a differently-cased or slash-separated name produces this.", evidence: EVIDENCE });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (header.includesFileNames) {
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
if (f.name === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
const expected = tesHash(f.name, "file");
|
|
158
|
+
if (expected !== f.hash)
|
|
159
|
+
findings.push({ validatorId: this.id, severity: "block", file, message: `${name}: file "${f.name}" in folder ${folders[f.folder]?.name ?? f.folder} is recorded with hash ${hex(f.hash)}, but its name hashes to ${hex(expected)}; the game looks files up by hash, so it cannot be found by name.`, fix: "Repack the archive.", evidence: EVIDENCE });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return cap(findings, this.id, file);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
export const ARCHIVE_RECORD_VALIDATORS = [archiveRecords, archiveHashOrder, archiveHashesMatch];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { BSA_VERSION_OBLIVION, BSA_VERSION_SKYRIM_LE, BSA_VERSION_SKYRIM_SE, BsaHeaderError, readBsaHeader } from "../archives.js";
|
|
3
|
+
const EVIDENCE = ["ModWright research"];
|
|
4
|
+
const APPLIES = "every .bsa file under a project source";
|
|
5
|
+
function isWithin(parent, child) {
|
|
6
|
+
const rel = path.relative(parent, child);
|
|
7
|
+
return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
|
|
8
|
+
}
|
|
9
|
+
function isBsaUnderSource(target, project) {
|
|
10
|
+
if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ".bsa")
|
|
11
|
+
return false;
|
|
12
|
+
return project.sources.some((s) => s.exists && isWithin(s.absPath, target.path));
|
|
13
|
+
}
|
|
14
|
+
async function headerOrFinding(id, file) {
|
|
15
|
+
try {
|
|
16
|
+
return await readBsaHeader(file);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const reason = err instanceof BsaHeaderError ? err.reason : "unreadable";
|
|
20
|
+
return {
|
|
21
|
+
validatorId: id,
|
|
22
|
+
severity: "block",
|
|
23
|
+
file,
|
|
24
|
+
message: `${path.basename(file)} has no readable BSA header (${reason}): ${err.message}`,
|
|
25
|
+
fix: "Repack the archive with BSArch or the Creation Kit's archive tool.",
|
|
26
|
+
evidence: EVIDENCE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const isFinding = (v) => v.validatorId !== undefined;
|
|
31
|
+
const archiveVersion = {
|
|
32
|
+
id: "sse.archives.version",
|
|
33
|
+
game: "skyrimse",
|
|
34
|
+
groups: ["plugins"],
|
|
35
|
+
modTypes: ["packaging"],
|
|
36
|
+
summary: "A .bsa carries version 105, the Skyrim SE format (LZ4, 24-byte folder records); 104 is Skyrim LE's and 103 Oblivion's.",
|
|
37
|
+
appliesTo: isBsaUnderSource,
|
|
38
|
+
appliesToDescription: APPLIES,
|
|
39
|
+
needs: { project: true },
|
|
40
|
+
cost: "edit",
|
|
41
|
+
severity: "block",
|
|
42
|
+
evidence: EVIDENCE,
|
|
43
|
+
async run(ctx) {
|
|
44
|
+
if (ctx.target.kind !== "file")
|
|
45
|
+
return [];
|
|
46
|
+
const header = await headerOrFinding(this.id, ctx.target.path);
|
|
47
|
+
if (isFinding(header))
|
|
48
|
+
return [header];
|
|
49
|
+
if (header.version === BSA_VERSION_SKYRIM_SE)
|
|
50
|
+
return [];
|
|
51
|
+
const name = path.basename(ctx.target.path);
|
|
52
|
+
const known = header.version === BSA_VERSION_SKYRIM_LE ? "Skyrim Legendary Edition's (104, zlib, 16-byte folder records)" : header.version === BSA_VERSION_OBLIVION ? "Oblivion's (103)" : undefined;
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
validatorId: this.id,
|
|
56
|
+
severity: "block",
|
|
57
|
+
file: ctx.target.path,
|
|
58
|
+
message: known
|
|
59
|
+
? `${name} is a version ${header.version} archive, ${known}; Skyrim SE reads version 105.`
|
|
60
|
+
: `${name} has BSA version ${header.version}; Skyrim SE reads version 105.`,
|
|
61
|
+
fix: "Extract and repack the archive for Skyrim SE (BSArch with the SSE format, or the Creation Kit).",
|
|
62
|
+
evidence: EVIDENCE,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const archiveNameFlags = {
|
|
68
|
+
id: "sse.archives.name-flags",
|
|
69
|
+
game: "skyrimse",
|
|
70
|
+
groups: ["plugins"],
|
|
71
|
+
modTypes: ["packaging"],
|
|
72
|
+
summary: "A .bsa sets both Include Directory Names (0x1) and Include File Names (0x2); every official archive does, and the game may not load one without them.",
|
|
73
|
+
appliesTo: isBsaUnderSource,
|
|
74
|
+
appliesToDescription: APPLIES,
|
|
75
|
+
needs: { project: true },
|
|
76
|
+
cost: "edit",
|
|
77
|
+
severity: "warn",
|
|
78
|
+
evidence: EVIDENCE,
|
|
79
|
+
async run(ctx) {
|
|
80
|
+
if (ctx.target.kind !== "file")
|
|
81
|
+
return [];
|
|
82
|
+
const header = await headerOrFinding(this.id, ctx.target.path);
|
|
83
|
+
if (isFinding(header))
|
|
84
|
+
return [header];
|
|
85
|
+
const missing = [...(header.includesDirectoryNames ? [] : ["Include Directory Names (0x1)"]), ...(header.includesFileNames ? [] : ["Include File Names (0x2)"])];
|
|
86
|
+
const findings = [];
|
|
87
|
+
if (missing.length > 0) {
|
|
88
|
+
findings.push({
|
|
89
|
+
validatorId: this.id,
|
|
90
|
+
severity: "warn",
|
|
91
|
+
file: ctx.target.path,
|
|
92
|
+
message: `${path.basename(ctx.target.path)} lacks ${missing.join(" and ")} in its archive flags; UESP notes the game may not load a BSA without them, and every official archive sets both.`,
|
|
93
|
+
fix: "Repack with names included (BSArch's default).",
|
|
94
|
+
evidence: EVIDENCE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (header.xbox360) {
|
|
98
|
+
findings.push({
|
|
99
|
+
validatorId: this.id,
|
|
100
|
+
severity: "warn",
|
|
101
|
+
file: ctx.target.path,
|
|
102
|
+
message: `${path.basename(ctx.target.path)} has the Xbox 360 flag (0x40): big-endian numbers and hashes, which the PC game does not read.`,
|
|
103
|
+
fix: "Repack for PC.",
|
|
104
|
+
evidence: EVIDENCE,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
export const ARCHIVE_VALIDATORS = [archiveVersion, archiveNameFlags];
|