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,321 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { walk } from "../../../core/fsutil.js";
|
|
3
|
+
import { ACTIONS, conditionTokenName, CURRENT_FORMAT, FORMAT_EDITDATA_FROMFILE_REMOVED, FORMAT_ENABLED_OBSOLETE, FORMAT_SCHEMA_MESSAGE, formatOlderThan, formatState, hasTokens, isKnownToken, parseContentJson, patchesOf, pick, splitLexically, tokenNames, tokenPathToRegExp, } from "../contentpatcher.js";
|
|
4
|
+
const EVIDENCE = ["stardewvalley.contentpatcher.format.what-a-pack-is", "stardewvalley.contentpatcher.format.published-json-schema"];
|
|
5
|
+
const APPLIES = "every content.json under a project source";
|
|
6
|
+
function isWithin(parent, child) {
|
|
7
|
+
const rel = path.relative(parent, child);
|
|
8
|
+
return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
|
|
9
|
+
}
|
|
10
|
+
function isContentJson(target, project) {
|
|
11
|
+
if (target.kind !== "file" || path.basename(target.path).toLowerCase() !== "content.json")
|
|
12
|
+
return false;
|
|
13
|
+
return project.sources.some((s) => s.exists && isWithin(s.absPath, target.path));
|
|
14
|
+
}
|
|
15
|
+
async function load(ctx, id, file) {
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = await ctx.readText(file);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return { validatorId: id, severity: "block", file, message: `content.json could not be read: ${err.message}`, evidence: EVIDENCE };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return parseContentJson(text);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { validatorId: id, severity: "block", file, message: `content.json is not a JSON object: ${err.message}; Content Patcher logs "Error preloading content pack" and loads nothing from it.`, fix: "Check it at smapi.io/json with the Content Patcher format.", evidence: EVIDENCE };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const isFinding = (v) => v.validatorId !== undefined;
|
|
31
|
+
const has = (p, name) => {
|
|
32
|
+
const v = pick(p.raw, name)?.value;
|
|
33
|
+
if (v === undefined || v === null)
|
|
34
|
+
return false;
|
|
35
|
+
if (typeof v === "string")
|
|
36
|
+
return v.trim().length > 0;
|
|
37
|
+
if (Array.isArray(v))
|
|
38
|
+
return v.length > 0;
|
|
39
|
+
if (typeof v === "object")
|
|
40
|
+
return Object.keys(v).length > 0;
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
const actionOf = (p) => ACTIONS.find((a) => a.toLowerCase() === (p.action ?? "").trim().toLowerCase());
|
|
44
|
+
function rule(id, summary, severity, evidence, run) {
|
|
45
|
+
return {
|
|
46
|
+
id: `stardewvalley.content.${id}`,
|
|
47
|
+
game: "stardewvalley",
|
|
48
|
+
groups: ["content"],
|
|
49
|
+
modTypes: ["content-pack"],
|
|
50
|
+
summary,
|
|
51
|
+
appliesTo: isContentJson,
|
|
52
|
+
appliesToDescription: APPLIES,
|
|
53
|
+
needs: { project: true },
|
|
54
|
+
cost: "edit",
|
|
55
|
+
severity,
|
|
56
|
+
evidence,
|
|
57
|
+
async run(ctx) {
|
|
58
|
+
if (ctx.target.kind !== "file")
|
|
59
|
+
return [];
|
|
60
|
+
const cfg = await load(ctx, this.id, ctx.target.path);
|
|
61
|
+
if (isFinding(cfg))
|
|
62
|
+
return id === "parses" ? [cfg] : [];
|
|
63
|
+
return run(ctx, cfg, ctx.target.path, this.id);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const parses = rule("parses", "content.json parses as a JSON object (comments and trailing commas allowed, as SMAPI's reader allows); anything else fails the whole pack.", "block", EVIDENCE, (_ctx, cfg, file, rid) => {
|
|
68
|
+
const findings = [];
|
|
69
|
+
for (const { key, expected } of cfg.miscased) {
|
|
70
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `top-level field "${key}" is read as "${expected}" (Content Patcher's JSON reader matches names case-insensitively), but the published schema and smapi.io/json reject the spelling.`, evidence: EVIDENCE });
|
|
71
|
+
}
|
|
72
|
+
if (cfg.extraFields.length > 0)
|
|
73
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `content.json has top-level fields Content Patcher does not define (${cfg.extraFields.join(", ")}); they are ignored, and smapi.io/json's schema rejects them.`, evidence: [...EVIDENCE, "stardewvalley.contentpatcher.format.top-level-fields"] });
|
|
74
|
+
return findings;
|
|
75
|
+
});
|
|
76
|
+
const format = rule("format", `Format is present and a version; below the current ${CURRENT_FORMAT} it still loads through Content Patcher's migrations (warn, never block, though the schema would reject it).`, "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.format-field-is-a-cp-version", "stardewvalley.contentpatcher.format.schema-pins-the-current-version", "stardewvalley.contentpatcher.format.whole-pack-rejection-messages"], (_ctx, cfg, file, rid) => {
|
|
77
|
+
switch (formatState(cfg.format)) {
|
|
78
|
+
case "missing":
|
|
79
|
+
return [{ validatorId: rid, severity: "block", file, message: "content pack doesn't specify the required Format field. (Content Patcher's own rejection; nothing in the pack loads.)", fix: `Add "Format": "${CURRENT_FORMAT}".`, evidence: EVIDENCE }];
|
|
80
|
+
case "invalid":
|
|
81
|
+
return [{ validatorId: rid, severity: "block", file, message: `Format ${JSON.stringify(cfg.format)} is not a version; Content Patcher cannot read the pack.`, fix: `Set "Format": "${CURRENT_FORMAT}".`, evidence: EVIDENCE }];
|
|
82
|
+
case "older":
|
|
83
|
+
return [{ validatorId: rid, severity: "warn", file, message: `Format "${cfg.format}" is older than the current ${CURRENT_FORMAT}: the pack still loads (Content Patcher migrates it), but the published schema rejects it with "${FORMAT_SCHEMA_MESSAGE}"`, fix: `Update to "Format": "${CURRENT_FORMAT}" and check the release notes for behaviour that changed in between.`, evidence: EVIDENCE }];
|
|
84
|
+
case "newer":
|
|
85
|
+
return [{ validatorId: rid, severity: "warn", file, message: `Format "${cfg.format}" is newer than ${CURRENT_FORMAT}, the current version when this rule was written; Content Patcher rejects a Format newer than itself.`, evidence: EVIDENCE }];
|
|
86
|
+
default:
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
const changesPresent = rule("changes-present", "Changes (a list of patch objects) or CustomLocations is present and non-empty; otherwise Content Patcher rejects the whole pack.", "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.whole-pack-rejection-messages", "stardewvalley.contentpatcher.format.top-level-fields"], (_ctx, cfg, file, rid) => {
|
|
91
|
+
const findings = [];
|
|
92
|
+
const changes = cfg.changes;
|
|
93
|
+
const locations = cfg.customLocations;
|
|
94
|
+
if (changes !== undefined && changes !== null && !Array.isArray(changes))
|
|
95
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Changes is ${Array.isArray(changes) ? "a list" : typeof changes}, not a list of patches.`, evidence: EVIDENCE });
|
|
96
|
+
if (locations !== undefined && locations !== null && !Array.isArray(locations))
|
|
97
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `CustomLocations is ${typeof locations}, not a list.`, evidence: EVIDENCE });
|
|
98
|
+
const entries = Array.isArray(changes) ? changes : [];
|
|
99
|
+
const anyChanges = entries.some((e) => e !== null);
|
|
100
|
+
const anyLocations = Array.isArray(locations) && locations.length > 0;
|
|
101
|
+
if (!anyChanges && !anyLocations && findings.length === 0) {
|
|
102
|
+
findings.push({ validatorId: rid, severity: "block", file, message: "content pack must specify the Changes or CustomLocations fields. (Content Patcher's own rejection; nothing in the pack loads.)", fix: 'Add a "Changes": [ ... ] list with at least one patch.', evidence: EVIDENCE });
|
|
103
|
+
}
|
|
104
|
+
for (const i of patchesOf(cfg).invalid) {
|
|
105
|
+
if (entries[i] === null)
|
|
106
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `Changes entry #${i + 1} is null; Content Patcher drops it and loads the rest of the list, so it is a patch that quietly does not exist.`, fix: "Remove the null, or write the patch it was meant to be.", evidence: EVIDENCE });
|
|
107
|
+
else
|
|
108
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Changes entry #${i + 1} is not an object; Content Patcher cannot read the pack.`, evidence: EVIDENCE });
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
});
|
|
112
|
+
const patchAction = rule("patch-action", "Every patch sets Action to one of Load, EditData, EditImage, EditMap or Include (case-insensitively); otherwise Content Patcher skips it with the reason quoted here.", "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.five-actions", "stardewvalley.contentpatcher.format.patch-skip-reasons"], (_ctx, cfg, file, rid) => {
|
|
113
|
+
const findings = [];
|
|
114
|
+
for (const p of patchesOf(cfg).patches) {
|
|
115
|
+
const raw = pick(p.raw, "Action")?.value;
|
|
116
|
+
if (raw === undefined || raw === null || (typeof raw === "string" && raw.trim() === "")) {
|
|
117
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the Action field.`, evidence: EVIDENCE });
|
|
118
|
+
}
|
|
119
|
+
else if (typeof raw !== "string" || !actionOf(p)) {
|
|
120
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: invalid Action value '${typeof raw === "string" ? raw : JSON.stringify(raw)}', expected one of: ${ACTIONS.join(", ")}.`, evidence: EVIDENCE });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return findings;
|
|
124
|
+
});
|
|
125
|
+
const patchTarget = rule("patch-target", "Every patch except an Include sets Target, and an Include does not; a Target is an asset name with no Content/ prefix and no extension.", "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.patch-skip-reasons", "stardewvalley.contentpatcher.format.asset-names-have-no-extension"], (_ctx, cfg, file, rid) => {
|
|
126
|
+
const findings = [];
|
|
127
|
+
for (const p of patchesOf(cfg).patches) {
|
|
128
|
+
const action = actionOf(p);
|
|
129
|
+
if (!action)
|
|
130
|
+
continue;
|
|
131
|
+
const target = has(p, "Target") ? p.target : undefined;
|
|
132
|
+
if (action === "Include") {
|
|
133
|
+
if (target !== undefined)
|
|
134
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: can't use the Target field with an Include patch.`, evidence: EVIDENCE });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (target === undefined) {
|
|
138
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the Target field.`, fix: 'Target is the asset name, e.g. "Portraits/Abigail".', evidence: EVIDENCE });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const t of splitLexically(target)) {
|
|
142
|
+
const norm = t.replace(/\\/g, "/");
|
|
143
|
+
if (/^content\//i.test(norm))
|
|
144
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `${p.label}: Target "${t}" starts with Content/; the asset name never includes the Content folder (Content/Maps/spring_beach.xnb is the asset Maps/spring_beach), so this names nothing the game loads.`, evidence: EVIDENCE });
|
|
145
|
+
if (/\.xnb$/i.test(norm))
|
|
146
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `${p.label}: Target "${t}" ends in a file extension; the asset name never includes the extension or the language code. Content Patcher strips a trailing .xnb (ParseAssetName, "maintain legacy behavior"), so this one still resolves, but write the asset name.`, evidence: EVIDENCE });
|
|
147
|
+
else if (/\.(?:png|json|tbin|tmx)$/i.test(norm))
|
|
148
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `${p.label}: Target "${t}" ends in a file extension; the asset name never includes the extension or the language code, and only a trailing .xnb is stripped, so this names nothing the game loads.`, evidence: EVIDENCE });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
});
|
|
153
|
+
const patchFields = rule("patch-fields", "Each action has the fields Content Patcher requires of it (FromFile for Load, EditImage and Include; Entries/Fields/MoveEntries/TextOperations for EditData; one of six for EditMap), and no field it forbids; unknown patch fields are noted.", "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.patch-fields", "stardewvalley.contentpatcher.format.patch-skip-reasons"], (_ctx, cfg, file, rid) => {
|
|
154
|
+
const findings = [];
|
|
155
|
+
const editDataFromFileAllowed = formatOlderThan(cfg.format, FORMAT_EDITDATA_FROMFILE_REMOVED);
|
|
156
|
+
for (const p of patchesOf(cfg).patches) {
|
|
157
|
+
if (p.extraFields.length > 0)
|
|
158
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `${p.label} has fields Content Patcher does not define (${p.extraFields.join(", ")}); they are ignored, which usually means a misspelled field is doing nothing.`, evidence: EVIDENCE });
|
|
159
|
+
const action = actionOf(p);
|
|
160
|
+
if (!action)
|
|
161
|
+
continue;
|
|
162
|
+
const fromFileRaw = pick(p.raw, "FromFile")?.value;
|
|
163
|
+
if (typeof fromFileRaw === "string" && fromFileRaw.trim().length === 0) {
|
|
164
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the FromFile field for this action type.`, fix: "Give FromFile a path relative to the pack folder, or remove the field.", evidence: EVIDENCE });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const fromFile = has(p, "FromFile");
|
|
168
|
+
const dataFields = ["Entries", "Fields", "MoveEntries", "TextOperations"].filter((f) => has(p, f));
|
|
169
|
+
const exclusive = ["Entries", "Fields", "MoveEntries"].some((f) => has(p, f));
|
|
170
|
+
const text = JSON.stringify(p.raw);
|
|
171
|
+
switch (action) {
|
|
172
|
+
case "Load":
|
|
173
|
+
if (!fromFile)
|
|
174
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the FromFile field for a Load patch.`, evidence: EVIDENCE });
|
|
175
|
+
break;
|
|
176
|
+
case "EditImage":
|
|
177
|
+
if (!fromFile)
|
|
178
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the FromFile field for a Load patch. (Content Patcher's wording for an EditImage patch without FromFile.)`, evidence: EVIDENCE });
|
|
179
|
+
break;
|
|
180
|
+
case "Include":
|
|
181
|
+
if (!fromFile)
|
|
182
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must set the FromFile field for an Include patch.`, evidence: EVIDENCE });
|
|
183
|
+
break;
|
|
184
|
+
case "EditData":
|
|
185
|
+
if (editDataFromFileAllowed) {
|
|
186
|
+
if (dataFields.length === 0 && !fromFile)
|
|
187
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: one of Entries, Fields, MoveEntries, TextOperations, or FromFile must be specified for an 'EditData' change.`, evidence: EVIDENCE });
|
|
188
|
+
if (fromFile && exclusive)
|
|
189
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: FromFile is mutually exclusive with Entries, Fields, and MoveEntries.`, evidence: EVIDENCE });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (fromFile)
|
|
193
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: the FromFile field can't be used with an 'EditData' patch. (Allowed only below Format ${FORMAT_EDITDATA_FROMFILE_REMOVED}.)`, fix: "Put the entries in an Entries block, or use an Include patch for a separate file.", evidence: EVIDENCE });
|
|
194
|
+
if (dataFields.length === 0)
|
|
195
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: one of Entries, Fields, MoveEntries, or TextOperations must be specified for an 'EditData' change.`, evidence: EVIDENCE });
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case "EditMap":
|
|
199
|
+
if (!["AddNpcWarps", "AddWarps", "FromFile", "MapProperties", "MapTiles", "TextOperations"].some((f) => has(p, f)))
|
|
200
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: must specify at least one of AddNpcWarps, AddWarps, FromFile, MapProperties, MapTiles, or TextOperations.`, evidence: EVIDENCE });
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (!fromFile && /\{\{\s*FromFile\s*\}\}/i.test(text))
|
|
204
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: can't use the {{FromFile}} token because the patch has no FromFile field.`, evidence: EVIDENCE });
|
|
205
|
+
if (!has(p, "Target")) {
|
|
206
|
+
const used = ["Target", "TargetPathOnly", "TargetWithoutPath"].find((t) => new RegExp(`\\{\\{\\s*${t}\\s*\\}\\}`, "i").test(text));
|
|
207
|
+
if (used !== undefined)
|
|
208
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `Ignored ${p.label}: can't use the {{${used}}} token because the patch has no Target field.`, evidence: EVIDENCE });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
});
|
|
213
|
+
async function packFiles(dir) {
|
|
214
|
+
try {
|
|
215
|
+
return (await walk(dir)).files;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const fromFileExists = rule("fromfile-exists", "Every FromFile names a file in the pack: a tokenless path must exist (a case-only mismatch warns, since Linux and macOS are case-sensitive), and a tokenised path must match at least one file. A missing file is a silent no-op Content Patcher reports only at Warn, at apply time.", "block", [...EVIDENCE, "stardewvalley.contentpatcher.format.patch-fields", "stardewvalley.contentpatcher.format.tokens-and-conditions"], async (ctx, cfg, file, rid) => {
|
|
222
|
+
const findings = [];
|
|
223
|
+
const packDir = path.dirname(file);
|
|
224
|
+
let listing;
|
|
225
|
+
const files = async () => (listing ??= await packFiles(packDir));
|
|
226
|
+
for (const p of patchesOf(cfg).patches) {
|
|
227
|
+
if (!has(p, "FromFile") || !p.fromFile)
|
|
228
|
+
continue;
|
|
229
|
+
for (const raw of splitLexically(p.fromFile)) {
|
|
230
|
+
const rel = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
231
|
+
if (!hasTokens(rel)) {
|
|
232
|
+
if (path.isAbsolute(rel) || rel.split("/").includes("..")) {
|
|
233
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `${p.label}: FromFile "${raw}" leaves the pack folder; Content Patcher resolves FromFile relative to the pack and refuses paths outside it.`, evidence: EVIDENCE });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const abs = path.join(packDir, rel);
|
|
237
|
+
const listed = await files();
|
|
238
|
+
if (listed.includes(rel))
|
|
239
|
+
continue;
|
|
240
|
+
const actual = listed.find((f) => f.toLowerCase() === rel.toLowerCase());
|
|
241
|
+
if (actual !== undefined) {
|
|
242
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `${p.label}: FromFile "${raw}" is on disk as "${actual}"; the case differs, which loads on Windows and fails on Linux and macOS unless SMAPI's UseCaseInsensitivePaths is on.`, fix: `Rename one to match the other exactly.`, evidence: EVIDENCE, related: [path.join(packDir, actual)] });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (await ctx.exists(abs))
|
|
246
|
+
continue;
|
|
247
|
+
findings.push({ validatorId: rid, severity: "block", file, message: `${p.label}: FromFile "${raw}" does not exist in the pack (${abs}); the patch applies nothing, and Content Patcher reports it only as one Warn line when the asset is requested.`, fix: "Add the file, or correct FromFile (paths are relative to the folder holding content.json, assets/ by convention).", evidence: EVIDENCE, related: [abs] });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const re = tokenPathToRegExp(rel);
|
|
251
|
+
const matches = (await files()).filter((f) => re.test(f));
|
|
252
|
+
if (matches.length === 0) {
|
|
253
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `${p.label}: FromFile "${raw}" uses tokens and no file in the pack matches the shape it can produce (${re.source}); unless every value is provided elsewhere, the patch applies nothing.`, fix: "Add at least one file the tokens can resolve to, or check the path.", evidence: EVIDENCE });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return findings;
|
|
258
|
+
});
|
|
259
|
+
const patchEnabled = rule("patch-enabled", `A patch's Enabled field: obsolete from Format ${FORMAT_ENABLED_OBSOLETE} on (Content Patcher skips the patch unless the value is literally true), and below that a false value skips the patch with no log line at all.`, "warn", [...EVIDENCE, "stardewvalley.contentpatcher.format.disabled-patch-logs-nothing", "stardewvalley.contentpatcher.format.patch-skip-reasons"], (_ctx, cfg, file, rid) => {
|
|
260
|
+
const findings = [];
|
|
261
|
+
const obsolete = !formatOlderThan(cfg.format, FORMAT_ENABLED_OBSOLETE);
|
|
262
|
+
for (const p of patchesOf(cfg).patches) {
|
|
263
|
+
const hit = pick(p.raw, "Enabled");
|
|
264
|
+
if (!hit || hit.value === undefined || hit.value === null)
|
|
265
|
+
continue;
|
|
266
|
+
const v = hit.value;
|
|
267
|
+
const literalTrue = v === true || (typeof v === "string" && v.trim().toLowerCase() === "true");
|
|
268
|
+
const literalFalse = v === false || (typeof v === "string" && v.trim().toLowerCase() === "false");
|
|
269
|
+
if (obsolete) {
|
|
270
|
+
if (literalTrue)
|
|
271
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `${p.label} sets Enabled: true, which Content Patcher ignores from Format ${FORMAT_ENABLED_OBSOLETE} on; remove it.`, evidence: EVIDENCE });
|
|
272
|
+
else
|
|
273
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `Ignored ${p.label}: the Enabled field is obsolete and should be removed. (From Format ${FORMAT_ENABLED_OBSOLETE} on, any value but true skips the patch.)`, fix: "Delete the Enabled field; use a When condition to switch the patch off.", evidence: EVIDENCE });
|
|
274
|
+
}
|
|
275
|
+
else if (literalFalse) {
|
|
276
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `${p.label} is disabled (Enabled: false) and Content Patcher logs nothing about it; the classic "my patch does nothing and the log is clean" cause.`, evidence: EVIDENCE });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return findings;
|
|
280
|
+
});
|
|
281
|
+
const whenTokens = rule("when-tokens", "Every When key names a token Content Patcher provides or the pack declares (ConfigSchema, DynamicTokens, AliasTokenNames); a mod-provided ModId/Name token is not checked. Content Patcher skips the patch with the reason quoted here.", "warn", [...EVIDENCE, "stardewvalley.contentpatcher.format.tokens-and-conditions", "stardewvalley.contentpatcher.format.patch-skip-reasons"], (_ctx, cfg, file, rid) => {
|
|
282
|
+
const findings = [];
|
|
283
|
+
for (const p of patchesOf(cfg).patches) {
|
|
284
|
+
const when = pick(p.raw, "When")?.value;
|
|
285
|
+
if (!when || typeof when !== "object" || Array.isArray(when))
|
|
286
|
+
continue;
|
|
287
|
+
for (const key of Object.keys(when)) {
|
|
288
|
+
const name = conditionTokenName(key);
|
|
289
|
+
if (name === undefined || isKnownToken(name, cfg))
|
|
290
|
+
continue;
|
|
291
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `Ignored ${p.label}: the When field is invalid: '${name}' can't be used as a token because that token could not be found.`, fix: "Check the spelling against the tokens guide, or declare it in ConfigSchema or DynamicTokens.", evidence: EVIDENCE });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return findings;
|
|
295
|
+
});
|
|
296
|
+
const configSchema = rule("config-schema", "Each ConfigSchema key is used somewhere in content.json (as a {{token}} or a When key); an unused one is a config.json option that changes nothing, and an entry that is not an object stops the whole pack from loading.", "warn", [...EVIDENCE, "stardewvalley.contentpatcher.format.config-json-and-schema"], (_ctx, cfg, file, rid) => {
|
|
297
|
+
if (!cfg.configSchema)
|
|
298
|
+
return [];
|
|
299
|
+
const findings = [];
|
|
300
|
+
const used = new Set();
|
|
301
|
+
const text = JSON.stringify(cfg.changes ?? []) + JSON.stringify(cfg.dynamicTokens ?? []) + JSON.stringify(cfg.customLocations ?? []);
|
|
302
|
+
for (const t of tokenNames(text))
|
|
303
|
+
used.add(t.toLowerCase());
|
|
304
|
+
for (const p of patchesOf(cfg).patches) {
|
|
305
|
+
const when = pick(p.raw, "When")?.value;
|
|
306
|
+
if (when && typeof when === "object" && !Array.isArray(when))
|
|
307
|
+
for (const k of Object.keys(when)) {
|
|
308
|
+
const name = conditionTokenName(k);
|
|
309
|
+
if (name)
|
|
310
|
+
used.add(name.toLowerCase());
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const [key, value] of Object.entries(cfg.configSchema)) {
|
|
314
|
+
if (!value || typeof value !== "object")
|
|
315
|
+
findings.push({ validatorId: rid, severity: "warn", file, message: `ConfigSchema "${key}" is not an object (each entry describes one config.json field: AllowValues, Default, ...); Content Patcher cannot read the file at all and logs "Error preloading content pack".`, evidence: EVIDENCE });
|
|
316
|
+
if (!used.has(key.toLowerCase()))
|
|
317
|
+
findings.push({ validatorId: rid, severity: "info", file, message: `ConfigSchema "${key}" is never used as {{${key}}} or as a When key, so the config.json option it creates changes nothing.`, evidence: EVIDENCE });
|
|
318
|
+
}
|
|
319
|
+
return findings;
|
|
320
|
+
});
|
|
321
|
+
export const CONTENT_VALIDATORS = [parses, format, changesPresent, patchAction, patchTarget, patchFields, fromFileExists, patchEnabled, whenTokens, configSchema];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { hasTokens, parseContentJson, patchesOf, pick, splitLexically } from "../contentpatcher.js";
|
|
3
|
+
import { normalizeAsset } from "../index/index.js";
|
|
4
|
+
const EVIDENCE = [
|
|
5
|
+
"stardewvalley.game.versions.data-asset-doc-index",
|
|
6
|
+
"stardewvalley.game.versions.1-6-moved-data-to-string-keyed-models",
|
|
7
|
+
"stardewvalley.wiki.dataformat.field-tables-are-the-schema",
|
|
8
|
+
"stardewvalley.wiki.dataformat.delimited-assets-use-index-tables",
|
|
9
|
+
];
|
|
10
|
+
const APPLIES = "every content.json under a project source";
|
|
11
|
+
const MAX_PER_FILE = 30;
|
|
12
|
+
function isWithin(parent, child) {
|
|
13
|
+
const rel = path.relative(parent, child);
|
|
14
|
+
return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
|
|
15
|
+
}
|
|
16
|
+
function isContentJson(target, project) {
|
|
17
|
+
if (target.kind !== "file" || path.basename(target.path).toLowerCase() !== "content.json")
|
|
18
|
+
return false;
|
|
19
|
+
return project.sources.some((s) => s.exists && isWithin(s.absPath, target.path));
|
|
20
|
+
}
|
|
21
|
+
const obj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : undefined);
|
|
22
|
+
function fieldNamesUsed(raw) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const fields = obj(pick(raw, "Fields")?.value);
|
|
25
|
+
if (fields)
|
|
26
|
+
for (const [entry, value] of Object.entries(fields))
|
|
27
|
+
for (const name of Object.keys(obj(value) ?? {}))
|
|
28
|
+
out.push({ name, from: `Fields > ${entry}` });
|
|
29
|
+
const entries = obj(pick(raw, "Entries")?.value);
|
|
30
|
+
if (entries)
|
|
31
|
+
for (const [entry, value] of Object.entries(entries))
|
|
32
|
+
for (const name of Object.keys(obj(value) ?? {}))
|
|
33
|
+
out.push({ name, from: `Entries > ${entry}` });
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const editDataFields = {
|
|
37
|
+
id: "stardewvalley.content.editdata-fields",
|
|
38
|
+
game: "stardewvalley",
|
|
39
|
+
groups: ["content"],
|
|
40
|
+
modTypes: ["content-pack"],
|
|
41
|
+
summary: "Every field an EditData patch sets on a documented Data/* asset (Fields keys, and the keys of object-valued Entries) is in the wiki's documented model for that asset; on a delimited-string asset the Fields keys are positions the format has. A case-only difference, a patch that sets TargetField, and an undocumented asset are noted, not guessed.",
|
|
42
|
+
appliesTo: isContentJson,
|
|
43
|
+
appliesToDescription: APPLIES,
|
|
44
|
+
needs: { project: true, index: true },
|
|
45
|
+
cost: "edit",
|
|
46
|
+
severity: "warn",
|
|
47
|
+
evidence: EVIDENCE,
|
|
48
|
+
async run(ctx) {
|
|
49
|
+
if (ctx.target.kind !== "file")
|
|
50
|
+
return [];
|
|
51
|
+
const file = ctx.target.path;
|
|
52
|
+
let text;
|
|
53
|
+
try {
|
|
54
|
+
text = await ctx.readText(file);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
let patches;
|
|
60
|
+
try {
|
|
61
|
+
patches = patchesOf(parseContentJson(text)).patches;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const findings = [];
|
|
67
|
+
const assets = new Map();
|
|
68
|
+
const lookupAsset = async (asset) => {
|
|
69
|
+
if (assets.has(asset))
|
|
70
|
+
return assets.get(asset);
|
|
71
|
+
const result = await ctx.lookup("asset", { name: asset, limit: 2000 });
|
|
72
|
+
let doc;
|
|
73
|
+
if (result.count > 0) {
|
|
74
|
+
doc = { shape: String(result.rows[0].shape ?? "model"), names: new Map(), maxPosition: -1 };
|
|
75
|
+
for (const r of result.rows) {
|
|
76
|
+
const name = String(r.name);
|
|
77
|
+
doc.names.set(name.toLowerCase(), name);
|
|
78
|
+
if (typeof r.position === "number" && r.position > doc.maxPosition)
|
|
79
|
+
doc.maxPosition = r.position;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
assets.set(asset, doc);
|
|
83
|
+
return doc;
|
|
84
|
+
};
|
|
85
|
+
const undocumented = new Set();
|
|
86
|
+
const reRooted = [];
|
|
87
|
+
for (const p of patches) {
|
|
88
|
+
if ((p.action ?? "").trim().toLowerCase() !== "editdata" || !p.target)
|
|
89
|
+
continue;
|
|
90
|
+
if (pick(p.raw, "TargetField") !== undefined) {
|
|
91
|
+
reRooted.push(p.label);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const used = fieldNamesUsed(p.raw);
|
|
95
|
+
for (const one of splitLexically(p.target)) {
|
|
96
|
+
if (hasTokens(one))
|
|
97
|
+
continue;
|
|
98
|
+
const asset = normalizeAsset(one);
|
|
99
|
+
if (!/^data\//i.test(asset))
|
|
100
|
+
continue;
|
|
101
|
+
const doc = await lookupAsset(asset);
|
|
102
|
+
if (!doc) {
|
|
103
|
+
undocumented.add(asset);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (doc.shape === "delimited") {
|
|
107
|
+
const entries = obj(pick(p.raw, "Entries")?.value);
|
|
108
|
+
if (entries)
|
|
109
|
+
for (const [entry, value] of Object.entries(entries)) {
|
|
110
|
+
if (obj(value))
|
|
111
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: `${p.label}: Entries > ${entry} is an object, but ${asset} is documented as a '/'-delimited string (${doc.maxPosition + 1} positions); the entry must be a string.`, evidence: EVIDENCE });
|
|
112
|
+
}
|
|
113
|
+
for (const { name, from } of used) {
|
|
114
|
+
if (from.startsWith("Entries") || hasTokens(name))
|
|
115
|
+
continue;
|
|
116
|
+
if (!/^\d+$/.test(name))
|
|
117
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: `${p.label}: ${from} > "${name}" is not a position; ${asset} is a '/'-delimited string, so Fields keys are the 0-based indexes the wiki documents (0 to ${doc.maxPosition}).`, evidence: EVIDENCE });
|
|
118
|
+
else if (Number(name) > doc.maxPosition)
|
|
119
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: `${p.label}: ${from} > position ${name} is beyond the documented format of ${asset} (positions 0 to ${doc.maxPosition}).`, evidence: EVIDENCE });
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const { name, from } of used) {
|
|
124
|
+
if (hasTokens(name))
|
|
125
|
+
continue;
|
|
126
|
+
const documented = doc.names.get(name.toLowerCase());
|
|
127
|
+
if (documented === name)
|
|
128
|
+
continue;
|
|
129
|
+
if (documented !== undefined)
|
|
130
|
+
findings.push({ validatorId: this.id, severity: "info", file, message: `${p.label}: ${from} > "${name}" is documented as "${documented}" on ${asset}; whether the game's JSON reader accepts the other casing is not established, so write the documented spelling.`, fix: `Use "${documented}".`, evidence: EVIDENCE });
|
|
131
|
+
else
|
|
132
|
+
findings.push({ validatorId: this.id, severity: "warn", file, message: `${p.label}: ${from} > "${name}" is not a field the wiki documents for ${asset} (${doc.names.size} documented); a misspelled field is silently ignored by the game.`, fix: `lookup asset name=${asset} lists the documented fields.`, evidence: EVIDENCE });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (reRooted.length > 0)
|
|
137
|
+
findings.push({ validatorId: this.id, severity: "info", file, message: `Field names were not checked for the patches that set TargetField: ${reRooted.join(", ")}. TargetField re-roots Fields and Entries at a sub-block, so their keys are not the asset's own model fields.`, evidence: EVIDENCE });
|
|
138
|
+
if (undocumented.size > 0)
|
|
139
|
+
findings.push({ validatorId: this.id, severity: "info", file, message: `EditData targets not documented by the wiki pages this index covers, so their field names were not checked: ${[...undocumented].sort().join(", ")}.`, evidence: EVIDENCE });
|
|
140
|
+
if (findings.length > MAX_PER_FILE) {
|
|
141
|
+
const rest = findings.length - MAX_PER_FILE;
|
|
142
|
+
return [...findings.slice(0, MAX_PER_FILE), { validatorId: this.id, severity: "info", file, message: `${rest} more field finding(s) in this file not listed.`, evidence: EVIDENCE }];
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
export const FIELDS_VALIDATORS = [editDataFields];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { metadataFor, readModMetadata } from "../compat.js";
|
|
3
|
+
import { readSmapiConfig, stringList } from "../config.js";
|
|
4
|
+
import { scanMods } from "../mods.js";
|
|
5
|
+
const EVIDENCE = ["stardewvalley.smapi.loader.load-order-override-lists", "stardewvalley.smapi.loader.load-order-override-warnings"];
|
|
6
|
+
const overrideLists = {
|
|
7
|
+
id: "stardewvalley.loadorder.override-lists",
|
|
8
|
+
game: "stardewvalley",
|
|
9
|
+
groups: ["packaging"],
|
|
10
|
+
modTypes: ["packaging"],
|
|
11
|
+
summary: "The ModsToLoadEarly and ModsToLoadLate lists in SMAPI's merged config name installed mods and share no id (SMAPI's three startup warnings, checked before launch); an id SMAPI's own metadata.json marks broken is noted.",
|
|
12
|
+
appliesTo: (target) => target.kind === "project",
|
|
13
|
+
appliesToDescription: "the project once, against the located install's smapi-internal/config.json (with config.user.json and Mods/SMAPI-config.json merged over it)",
|
|
14
|
+
needs: { project: true, install: true },
|
|
15
|
+
cost: "edit",
|
|
16
|
+
severity: "warn",
|
|
17
|
+
evidence: EVIDENCE,
|
|
18
|
+
async run(ctx) {
|
|
19
|
+
if (ctx.target.kind !== "project" || !ctx.install)
|
|
20
|
+
return [];
|
|
21
|
+
const install = ctx.install;
|
|
22
|
+
const config = await readSmapiConfig(install);
|
|
23
|
+
const findings = [];
|
|
24
|
+
if (config.files.length === 0) {
|
|
25
|
+
return [{ validatorId: this.id, severity: "info", file: path.join(install.root, "smapi-internal", "config.json"), message: "No SMAPI config file exists (smapi-internal/config.json, config.user.json or Mods/SMAPI-config.json): either SMAPI is not installed here, or it has never been installed by its installer. Its shipped default leaves both override lists empty.", evidence: EVIDENCE }];
|
|
26
|
+
}
|
|
27
|
+
for (const f of config.unreadable)
|
|
28
|
+
findings.push({ validatorId: this.id, severity: "warn", file: f, message: `${path.basename(f)} is not valid JSON; SMAPI cannot read it and, for the base config, will not start.`, evidence: EVIDENCE });
|
|
29
|
+
const dedupe = (ids) => {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
return ids.filter((id) => (seen.has(id.toLowerCase()) ? false : (seen.add(id.toLowerCase()), true)));
|
|
32
|
+
};
|
|
33
|
+
const early = dedupe(stringList(config.settings, "ModsToLoadEarly"));
|
|
34
|
+
const late = dedupe(stringList(config.settings, "ModsToLoadLate"));
|
|
35
|
+
if (early.length === 0 && late.length === 0)
|
|
36
|
+
return findings;
|
|
37
|
+
const modsDir = path.join(install.root, "Mods");
|
|
38
|
+
const mods = (await ctx.exists(modsDir)) ? await scanMods(modsDir) : [];
|
|
39
|
+
const installed = new Set();
|
|
40
|
+
for (const m of mods)
|
|
41
|
+
if (typeof m.manifest?.UniqueID === "string")
|
|
42
|
+
installed.add(m.manifest.UniqueID.toLowerCase());
|
|
43
|
+
const sortIds = (ids) => [...ids].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
44
|
+
const missingEarly = sortIds(early.filter((id) => !installed.has(id.toLowerCase())));
|
|
45
|
+
const missingLate = sortIds(late.filter((id) => !installed.has(id.toLowerCase())));
|
|
46
|
+
const earlyIds = new Set(early.map((id) => id.toLowerCase()));
|
|
47
|
+
const duplicate = sortIds(late.filter((id) => earlyIds.has(id.toLowerCase())));
|
|
48
|
+
const quote = (ids) => `'${ids.join("', '")}'`;
|
|
49
|
+
const fileFor = (key) => config.sources[key] ?? config.files[config.files.length - 1];
|
|
50
|
+
if (missingEarly.length > 0)
|
|
51
|
+
findings.push({ validatorId: this.id, severity: "warn", file: fileFor("ModsToLoadEarly"), message: `The 'smapi-internal/config.json' file lists mod IDs in ModsToLoadEarly which aren't installed: ${quote(missingEarly)}. (SMAPI's startup warning; the entry is ignored.)`, fix: "Remove the id, or install the mod; ids are the manifests' UniqueID, matched case-insensitively.", evidence: EVIDENCE });
|
|
52
|
+
if (missingLate.length > 0)
|
|
53
|
+
findings.push({ validatorId: this.id, severity: "warn", file: fileFor("ModsToLoadLate"), message: `The 'smapi-internal/config.json' file lists mod IDs in ModsToLoadLate which aren't installed: ${quote(missingLate)}. (SMAPI's startup warning; the entry is ignored.)`, fix: "Remove the id, or install the mod; ids are the manifests' UniqueID, matched case-insensitively.", evidence: EVIDENCE });
|
|
54
|
+
if (duplicate.length > 0)
|
|
55
|
+
findings.push({ validatorId: this.id, severity: "warn", file: fileFor("ModsToLoadLate"), message: `The 'smapi-internal/config.json' file lists mod IDs which are in both ModsToLoadEarly and ModsToLoadLate: ${quote(duplicate)}. These will be loaded early. (SMAPI's startup warning.)`, fix: "Keep the id in one list.", evidence: EVIDENCE });
|
|
56
|
+
const metadata = await readModMetadata(install);
|
|
57
|
+
if (metadata !== undefined) {
|
|
58
|
+
for (const id of [...early, ...late]) {
|
|
59
|
+
for (const e of metadataFor(metadata, id)) {
|
|
60
|
+
if (!/^(?:AssumeBroken|Obsolete)$/i.test(e.status))
|
|
61
|
+
continue;
|
|
62
|
+
findings.push({ validatorId: this.id, severity: "info", file: path.join(install.root, "smapi-internal", "metadata.json"), message: `${id} is in the override lists and SMAPI's own metadata.json marks "${e.name}" ${e.status}${e.range ? ` for versions ${e.range}` : ""}${e.reason ? `: ${e.reason}` : ""}; if the installed version is in that range SMAPI skips it regardless of order.`, evidence: [...EVIDENCE, "stardewvalley.smapi.loader.rewrites-mods-for-compatibility"] });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return findings;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const LOADORDER_VALIDATORS = [overrideLists];
|