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,1181 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { walk } from "../../../core/fsutil.js";
|
|
4
|
+
import { parseTweakXLYaml } from "../index/parsers/tweakxl-yaml.js";
|
|
5
|
+
import { locateTool, runTool } from "../../../core/toolchain/index.js";
|
|
6
|
+
import { TOOL_SPECS } from "../../../core/toolchain/specs.js";
|
|
7
|
+
import { cnameValue, collectDollarValueStrings, collectKey, collectResourcePaths, existsInVanillaIndex, existsStaged, isJsonObject, readCr2wJson, readStagedCr2wJson, resourcePathValue, walkObjects, } from "./cr2w.js";
|
|
8
|
+
async function existsStagedOrVanilla(ctx, absPath, depotPath) {
|
|
9
|
+
if (await existsStaged(ctx, absPath))
|
|
10
|
+
return true;
|
|
11
|
+
return existsInVanillaIndex(ctx, depotPath);
|
|
12
|
+
}
|
|
13
|
+
const KNOWN_PLACEHOLDERS = new Set([
|
|
14
|
+
"camera",
|
|
15
|
+
"gender",
|
|
16
|
+
"body",
|
|
17
|
+
"arms",
|
|
18
|
+
"feet",
|
|
19
|
+
"sleeves",
|
|
20
|
+
"skin_color",
|
|
21
|
+
"hair_color",
|
|
22
|
+
]);
|
|
23
|
+
function isKnownPlaceholder(token) {
|
|
24
|
+
if (KNOWN_PLACEHOLDERS.has(token))
|
|
25
|
+
return true;
|
|
26
|
+
return /^variant(\.\d+)?$/.test(token);
|
|
27
|
+
}
|
|
28
|
+
const ENUMERABLE_VALUES = {
|
|
29
|
+
camera: ["fpp", "tpp"],
|
|
30
|
+
gender: ["m", "w"],
|
|
31
|
+
arms: ["base_arms", "mantis_blades", "monowire", "projectile_launcher"],
|
|
32
|
+
feet: ["flat", "lifted", "high_heels", "flat_shoes"],
|
|
33
|
+
sleeves: ["full", "part"],
|
|
34
|
+
};
|
|
35
|
+
const PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_.]*)\}/g;
|
|
36
|
+
function placeholdersIn(text) {
|
|
37
|
+
const out = [];
|
|
38
|
+
let m;
|
|
39
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
40
|
+
while ((m = PLACEHOLDER_RE.exec(text)))
|
|
41
|
+
out.push(m[1]);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
const KNOWN_VISUAL_TAGS = new Set([
|
|
45
|
+
"hide_H1",
|
|
46
|
+
"hide_F1",
|
|
47
|
+
"hide_T1",
|
|
48
|
+
"hide_T2",
|
|
49
|
+
"hide_L1",
|
|
50
|
+
"hide_S1",
|
|
51
|
+
"hide_Hair",
|
|
52
|
+
"hide_Genitals",
|
|
53
|
+
"hide_T1part",
|
|
54
|
+
"hide_Head",
|
|
55
|
+
"hide_Torso",
|
|
56
|
+
"hide_LowerAbdomen",
|
|
57
|
+
"hide_UpperAbdomen",
|
|
58
|
+
"hide_CollarBone",
|
|
59
|
+
"hide_Arms",
|
|
60
|
+
"hide_Thighs",
|
|
61
|
+
"hide_Calves",
|
|
62
|
+
"hide_Ankles",
|
|
63
|
+
"hide_Feet",
|
|
64
|
+
"hide_Legs",
|
|
65
|
+
"AppearanceParts",
|
|
66
|
+
"HighHeels",
|
|
67
|
+
"FlatShoes",
|
|
68
|
+
"force_FlatFeet",
|
|
69
|
+
"force_Hair",
|
|
70
|
+
"DynamicAppearance",
|
|
71
|
+
]);
|
|
72
|
+
const KNOWN_OUTFIT_SLOTS = new Set([
|
|
73
|
+
"TorsoUnder",
|
|
74
|
+
"TorsoInner",
|
|
75
|
+
"TorsoMiddle",
|
|
76
|
+
"TorsoOuter",
|
|
77
|
+
"TorsoAux",
|
|
78
|
+
"LegsInner",
|
|
79
|
+
"LegsMiddle",
|
|
80
|
+
"LegsOuter",
|
|
81
|
+
"BodyUnder",
|
|
82
|
+
"BodyInner",
|
|
83
|
+
"BodyMiddle",
|
|
84
|
+
"BodyOuter",
|
|
85
|
+
"EarLeft",
|
|
86
|
+
"EarRight",
|
|
87
|
+
"EyeLeft",
|
|
88
|
+
"EyeRight",
|
|
89
|
+
"Eyes",
|
|
90
|
+
"ShoulderLeft",
|
|
91
|
+
"ShoulderRight",
|
|
92
|
+
"ElbowLeft",
|
|
93
|
+
"ElbowRight",
|
|
94
|
+
"WristLeft",
|
|
95
|
+
"WristRight",
|
|
96
|
+
"HandLeft",
|
|
97
|
+
"HandRight",
|
|
98
|
+
"Hands",
|
|
99
|
+
"FingersLeft",
|
|
100
|
+
"FingersRight",
|
|
101
|
+
"FingernailsLeft",
|
|
102
|
+
"FingernailsRight",
|
|
103
|
+
"ThighLeft",
|
|
104
|
+
"ThighRight",
|
|
105
|
+
"KneeLeft",
|
|
106
|
+
"KneeRight",
|
|
107
|
+
"AnkleLeft",
|
|
108
|
+
"AnkleRight",
|
|
109
|
+
"ToesLeft",
|
|
110
|
+
"ToesRight",
|
|
111
|
+
"ToenailsLeft",
|
|
112
|
+
"ToenailsRight",
|
|
113
|
+
"HandPropLeft",
|
|
114
|
+
"HandPropRight",
|
|
115
|
+
"Head",
|
|
116
|
+
"Glasses",
|
|
117
|
+
"Mask",
|
|
118
|
+
"Feet",
|
|
119
|
+
"NecklaceShort",
|
|
120
|
+
]);
|
|
121
|
+
function isEntJson(p) {
|
|
122
|
+
return p.toLowerCase().endsWith(".ent.json");
|
|
123
|
+
}
|
|
124
|
+
function isAppJson(p) {
|
|
125
|
+
return p.toLowerCase().endsWith(".app.json");
|
|
126
|
+
}
|
|
127
|
+
function isXlManifest(p) {
|
|
128
|
+
return p.toLowerCase().endsWith(".archive.xl");
|
|
129
|
+
}
|
|
130
|
+
function isYamlFile(p) {
|
|
131
|
+
return /\.ya?ml$/i.test(p);
|
|
132
|
+
}
|
|
133
|
+
function fileTarget(t) {
|
|
134
|
+
return t.kind === "file" ? t.path : undefined;
|
|
135
|
+
}
|
|
136
|
+
function toSnakeCase(name) {
|
|
137
|
+
return name
|
|
138
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
139
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
140
|
+
.toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
function stagingRoot(project) {
|
|
143
|
+
return project.sources.find((s) => s.kind === "archive-staging")?.absPath;
|
|
144
|
+
}
|
|
145
|
+
function resolveDepotPath(root, depotPath) {
|
|
146
|
+
const stripped = depotPath.startsWith("*") ? depotPath.slice(1) : depotPath;
|
|
147
|
+
return path.join(root, ...stripped.split(/[\\/]/));
|
|
148
|
+
}
|
|
149
|
+
async function allFilesUnder(project, predicate) {
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const source of project.sources) {
|
|
152
|
+
if (!source.exists)
|
|
153
|
+
continue;
|
|
154
|
+
const walked = await walk(source.absPath, { limit: 20_000 });
|
|
155
|
+
for (const rel of walked.files) {
|
|
156
|
+
if (predicate(rel))
|
|
157
|
+
out.push(path.join(source.absPath, rel));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
async function readYaml(ctx, absPath) {
|
|
163
|
+
try {
|
|
164
|
+
const text = await ctx.readText(absPath);
|
|
165
|
+
const YAML = await import("yaml");
|
|
166
|
+
return YAML.parse(text);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function xlManifests(ctx) {
|
|
173
|
+
const files = await allFilesUnder(ctx.project, isXlManifest);
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const parsed = await readYaml(ctx, file);
|
|
177
|
+
if (isJsonObject(parsed))
|
|
178
|
+
out.push({ file, manifest: parsed });
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function bodyValueSet(manifests) {
|
|
183
|
+
const values = new Set(["base_body"]);
|
|
184
|
+
for (const { manifest } of manifests) {
|
|
185
|
+
const player = manifest.player;
|
|
186
|
+
if (!isJsonObject(player))
|
|
187
|
+
continue;
|
|
188
|
+
const bodyTypes = player.bodyTypes;
|
|
189
|
+
if (!Array.isArray(bodyTypes))
|
|
190
|
+
continue;
|
|
191
|
+
for (const bt of bodyTypes) {
|
|
192
|
+
if (typeof bt === "string")
|
|
193
|
+
values.add(toSnakeCase(bt));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [...values];
|
|
197
|
+
}
|
|
198
|
+
function customTagNames(manifests) {
|
|
199
|
+
const out = new Set();
|
|
200
|
+
for (const { manifest } of manifests) {
|
|
201
|
+
const overrides = manifest.overrides;
|
|
202
|
+
if (!isJsonObject(overrides))
|
|
203
|
+
continue;
|
|
204
|
+
const tags = overrides.tags;
|
|
205
|
+
if (!isJsonObject(tags))
|
|
206
|
+
continue;
|
|
207
|
+
for (const key of Object.keys(tags))
|
|
208
|
+
out.add(key);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
const STAR_PREFIX_EVIDENCE = ["cp2077.clothing.garments.substitution-path-requires-star-prefix"];
|
|
213
|
+
export const GARMENT_STAR_PREFIX = {
|
|
214
|
+
id: "cp2077.garments.substitution-star-prefix",
|
|
215
|
+
game: "cyberpunk2077",
|
|
216
|
+
groups: ["garments"],
|
|
217
|
+
modTypes: ["garment", "refit"],
|
|
218
|
+
summary: "A mesh-entity component depotPath containing {…} must start with *.",
|
|
219
|
+
appliesTo: (t) => {
|
|
220
|
+
const f = fileTarget(t);
|
|
221
|
+
return f !== undefined && (isEntJson(f) || isAppJson(f));
|
|
222
|
+
},
|
|
223
|
+
appliesToDescription: "every .ent.json/.app.json file",
|
|
224
|
+
needs: { project: true },
|
|
225
|
+
cost: "edit",
|
|
226
|
+
severity: "block",
|
|
227
|
+
evidence: STAR_PREFIX_EVIDENCE,
|
|
228
|
+
async run(ctx) {
|
|
229
|
+
const file = fileTarget(ctx.target);
|
|
230
|
+
if (!file)
|
|
231
|
+
return [];
|
|
232
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
233
|
+
if (!parsed)
|
|
234
|
+
return [];
|
|
235
|
+
const findings = [];
|
|
236
|
+
for (const { path: p, jsonPath } of collectResourcePaths(parsed.root)) {
|
|
237
|
+
if (p.includes("{") && !p.startsWith("*")) {
|
|
238
|
+
findings.push({
|
|
239
|
+
validatorId: this.id,
|
|
240
|
+
severity: "block",
|
|
241
|
+
file,
|
|
242
|
+
message: `Depot path "${p}" at ${jsonPath} contains a {…} substitution placeholder but does not start with "*". ` +
|
|
243
|
+
"ArchiveXL only substitutes {placeholder} in a mesh-entity component's depot path when the path starts with *; " +
|
|
244
|
+
"without it, the literal, unsubstituted path is looked up and the game crashes loading a mesh that does not exist.",
|
|
245
|
+
fix: `Prefix the path with "*": "*${p}".`,
|
|
246
|
+
evidence: STAR_PREFIX_EVIDENCE,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return findings;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const PLACEHOLDER_KNOWN_EVIDENCE = [
|
|
254
|
+
"cp2077.clothing.garments.dynamic-appearance-placeholders",
|
|
255
|
+
"cp2077.clothing.garments.dynamic-variant-name-syntax",
|
|
256
|
+
];
|
|
257
|
+
export const GARMENT_PLACEHOLDER_KNOWN = {
|
|
258
|
+
id: "cp2077.garments.placeholder-known",
|
|
259
|
+
game: "cyberpunk2077",
|
|
260
|
+
groups: ["garments"],
|
|
261
|
+
modTypes: ["garment", "refit"],
|
|
262
|
+
summary: "Every {…} placeholder used is one of ArchiveXL's documented substitutions.",
|
|
263
|
+
appliesTo: (t) => {
|
|
264
|
+
const f = fileTarget(t);
|
|
265
|
+
return f !== undefined && (isEntJson(f) || isAppJson(f));
|
|
266
|
+
},
|
|
267
|
+
appliesToDescription: "every .ent.json/.app.json file",
|
|
268
|
+
needs: {},
|
|
269
|
+
cost: "edit",
|
|
270
|
+
severity: "block",
|
|
271
|
+
evidence: PLACEHOLDER_KNOWN_EVIDENCE,
|
|
272
|
+
async run(ctx) {
|
|
273
|
+
const file = fileTarget(ctx.target);
|
|
274
|
+
if (!file)
|
|
275
|
+
return [];
|
|
276
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
277
|
+
if (!parsed)
|
|
278
|
+
return [];
|
|
279
|
+
const findings = [];
|
|
280
|
+
const seen = new Set();
|
|
281
|
+
for (const { value, jsonPath } of collectDollarValueStrings(parsed.root)) {
|
|
282
|
+
for (const token of placeholdersIn(value)) {
|
|
283
|
+
if (isKnownPlaceholder(token))
|
|
284
|
+
continue;
|
|
285
|
+
const key = `${token}@${jsonPath}`;
|
|
286
|
+
if (seen.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(key);
|
|
289
|
+
findings.push({
|
|
290
|
+
validatorId: this.id,
|
|
291
|
+
severity: "block",
|
|
292
|
+
file,
|
|
293
|
+
message: `"{${token}}" at ${jsonPath} (in "${value}") is not a placeholder ModWright's knowledge base attests as of ` +
|
|
294
|
+
"the wiki's 2026-09-07 reading (camera, gender, body, arms, feet, sleeves, skin_color, hair_color, variant/variant.N). " +
|
|
295
|
+
"It may be a real, newer ArchiveXL substitution unknown to this check, or a typo — check the spelling first.",
|
|
296
|
+
fix: "Use one of camera, gender, body, arms, feet, sleeves, skin_color, hair_color, variant, variant.1, variant.2.",
|
|
297
|
+
evidence: PLACEHOLDER_KNOWN_EVIDENCE,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return findings;
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
const COMBINATION_EVIDENCE = [
|
|
305
|
+
"cp2077.clothing.garments.unvalidated-variant-crashes",
|
|
306
|
+
"cp2077.clothing.refits.default-body-tag-is-base-body",
|
|
307
|
+
];
|
|
308
|
+
const COMBINATION_CAP = 64;
|
|
309
|
+
export const GARMENT_COMBINATION_RESOLVES = {
|
|
310
|
+
id: "cp2077.garments.substitution-combination-resolves",
|
|
311
|
+
game: "cyberpunk2077",
|
|
312
|
+
groups: ["garments"],
|
|
313
|
+
modTypes: ["garment", "refit"],
|
|
314
|
+
summary: "Every combination of the placeholders actually present in a substituted path resolves to a staged file or the vanilla archive index.",
|
|
315
|
+
appliesTo: (t) => t.kind === "project",
|
|
316
|
+
appliesToDescription: "once per validation, over every *-prefixed substituted depotPath in the project's .ent.json files",
|
|
317
|
+
needs: { project: true },
|
|
318
|
+
cost: "build",
|
|
319
|
+
severity: "block",
|
|
320
|
+
evidence: COMBINATION_EVIDENCE,
|
|
321
|
+
async run(ctx) {
|
|
322
|
+
const root = stagingRoot(ctx.project);
|
|
323
|
+
if (!root)
|
|
324
|
+
return [];
|
|
325
|
+
const manifests = await xlManifests(ctx);
|
|
326
|
+
const values = { ...ENUMERABLE_VALUES, body: bodyValueSet(manifests) };
|
|
327
|
+
const entFiles = await allFilesUnder(ctx.project, isEntJson);
|
|
328
|
+
const findings = [];
|
|
329
|
+
let capped = false;
|
|
330
|
+
for (const file of entFiles) {
|
|
331
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
332
|
+
if (!parsed)
|
|
333
|
+
continue;
|
|
334
|
+
for (const { path: p, jsonPath } of collectResourcePaths(parsed.root)) {
|
|
335
|
+
if (!p.startsWith("*") || !p.includes("{"))
|
|
336
|
+
continue;
|
|
337
|
+
const tokens = [...new Set(placeholdersIn(p))];
|
|
338
|
+
if (tokens.length === 0)
|
|
339
|
+
continue;
|
|
340
|
+
if (tokens.some((t) => !isKnownPlaceholder(t) || !(t in values)))
|
|
341
|
+
continue;
|
|
342
|
+
const domains = tokens.map((t) => values[t]);
|
|
343
|
+
const total = domains.reduce((n, d) => n * d.length, 1);
|
|
344
|
+
if (total > COMBINATION_CAP) {
|
|
345
|
+
if (!capped) {
|
|
346
|
+
findings.push({
|
|
347
|
+
validatorId: this.id,
|
|
348
|
+
severity: "info",
|
|
349
|
+
file,
|
|
350
|
+
message: `${jsonPath} enumerates ${total} combinations of {${tokens.join("}{")}}, over the ${COMBINATION_CAP}-combination cap; skipped rather than reporting a partial answer as clean.`,
|
|
351
|
+
evidence: COMBINATION_EVIDENCE,
|
|
352
|
+
});
|
|
353
|
+
capped = true;
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
let combos = [[]];
|
|
358
|
+
for (const domain of domains) {
|
|
359
|
+
combos = combos.flatMap((prefix) => domain.map((v) => [...prefix, v]));
|
|
360
|
+
}
|
|
361
|
+
for (const combo of combos) {
|
|
362
|
+
let resolved = p;
|
|
363
|
+
tokens.forEach((t, i) => {
|
|
364
|
+
resolved = resolved.split(`{${t}}`).join(combo[i]);
|
|
365
|
+
});
|
|
366
|
+
const abs = resolveDepotPath(root, resolved);
|
|
367
|
+
if (!(await existsStagedOrVanilla(ctx, abs, resolved))) {
|
|
368
|
+
findings.push({
|
|
369
|
+
validatorId: this.id,
|
|
370
|
+
severity: "block",
|
|
371
|
+
file,
|
|
372
|
+
message: `The combination {${tokens.map((t, i) => `${t}=${combo[i]}`).join(", ")}} of ${jsonPath} resolves to ` +
|
|
373
|
+
`"${resolved}", which is not staged (expected at ${abs}) and does not resolve in the vanilla archive index. ` +
|
|
374
|
+
"ArchiveXL crashes the game loading a mesh that does not exist for whichever combination the player's install produces.",
|
|
375
|
+
fix: `Stage a mesh at "${resolved}", or narrow the {${tokens.join("}/{")}} this component actually supports.`,
|
|
376
|
+
evidence: COMBINATION_EVIDENCE,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return findings;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
const BODY_TAG_EVIDENCE = [
|
|
386
|
+
"cp2077.clothing.refits.bodytypes-key-snake-cases-name",
|
|
387
|
+
"cp2077.clothing.refits.body-tag-patches-player-ent",
|
|
388
|
+
"cp2077.clothing.refits.body-patch-lists-gendered-player-ents",
|
|
389
|
+
"cp2077.clothing.refits.default-body-tag-is-base-body",
|
|
390
|
+
];
|
|
391
|
+
export const GARMENT_BODY_TAG_REGISTRATION = {
|
|
392
|
+
id: "cp2077.garments.body-tag-registration",
|
|
393
|
+
game: "cyberpunk2077",
|
|
394
|
+
groups: ["garments"],
|
|
395
|
+
modTypes: ["garment", "refit"],
|
|
396
|
+
summary: "A .xl declaring player.bodyTypes ships a patch .ent with a matching Body:<Name> component.",
|
|
397
|
+
appliesTo: (t) => t.kind === "project",
|
|
398
|
+
appliesToDescription: "once per validation, when a project's .xl declares player.bodyTypes",
|
|
399
|
+
needs: { project: true },
|
|
400
|
+
cost: "build",
|
|
401
|
+
severity: "block",
|
|
402
|
+
evidence: BODY_TAG_EVIDENCE,
|
|
403
|
+
async run(ctx) {
|
|
404
|
+
const root = stagingRoot(ctx.project);
|
|
405
|
+
const manifests = await xlManifests(ctx);
|
|
406
|
+
const findings = [];
|
|
407
|
+
for (const { file: xlFile, manifest } of manifests) {
|
|
408
|
+
const player = manifest.player;
|
|
409
|
+
if (!isJsonObject(player) || !Array.isArray(player.bodyTypes))
|
|
410
|
+
continue;
|
|
411
|
+
const patch = isJsonObject(manifest.resource) ? manifest.resource.patch : undefined;
|
|
412
|
+
const patchEntKeys = isJsonObject(patch) ? Object.keys(patch) : [];
|
|
413
|
+
for (const bodyType of player.bodyTypes) {
|
|
414
|
+
if (typeof bodyType !== "string")
|
|
415
|
+
continue;
|
|
416
|
+
const wantedName = `Body:${bodyType}`;
|
|
417
|
+
let found = false;
|
|
418
|
+
for (const depotKey of patchEntKeys) {
|
|
419
|
+
if (!root)
|
|
420
|
+
break;
|
|
421
|
+
const abs = resolveDepotPath(root, depotKey);
|
|
422
|
+
const parsed = await readStagedCr2wJson(ctx, abs);
|
|
423
|
+
if (!parsed)
|
|
424
|
+
continue;
|
|
425
|
+
const names = collectKey(parsed.root, "name").map((h) => cnameValue(h.value));
|
|
426
|
+
if (names.includes(wantedName)) {
|
|
427
|
+
found = true;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!found) {
|
|
432
|
+
findings.push({
|
|
433
|
+
validatorId: this.id,
|
|
434
|
+
severity: "block",
|
|
435
|
+
file: xlFile,
|
|
436
|
+
message: `player.bodyTypes declares "${bodyType}", but no patch .ent registered under resource.patch has an ` +
|
|
437
|
+
`entMeshComponent named "${wantedName}". PuppetStateSystem's body-type lookup (and every {body} substitution ` +
|
|
438
|
+
`resolving to "${toSnakeCase(bodyType)}") depends on that component existing on the patched player entity.`,
|
|
439
|
+
fix: `Add an entMeshComponent named "${wantedName}" to the patch .ent listed under resource.patch in ${path.basename(xlFile)}.`,
|
|
440
|
+
evidence: BODY_TAG_EVIDENCE,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return findings;
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const VISUAL_TAGS_EVIDENCE = [
|
|
449
|
+
"cp2077.clothing.garments.vanilla-hide-tags-by-slot",
|
|
450
|
+
"cp2077.clothing.garments.archivexl-fine-grained-hide-tags",
|
|
451
|
+
"cp2077.clothing.garments.visual-tags-are-case-sensitive",
|
|
452
|
+
"cp2077.clothing.garments.custom-tag-declared-in-xl-not-yaml",
|
|
453
|
+
"cp2077.clothing.garments.force-hair-tag",
|
|
454
|
+
"cp2077.clothing.garments.heel-state-tags-need-foot-base",
|
|
455
|
+
];
|
|
456
|
+
function collectAppearanceComponentGroups(root) {
|
|
457
|
+
const groups = [];
|
|
458
|
+
walkObjects(root, (obj) => {
|
|
459
|
+
if (!Array.isArray(obj.components))
|
|
460
|
+
return;
|
|
461
|
+
const names = obj.components
|
|
462
|
+
.map((c) => (isJsonObject(c) ? cnameValue(c.name) : undefined))
|
|
463
|
+
.filter((n) => n !== undefined);
|
|
464
|
+
if (names.length > 0)
|
|
465
|
+
groups.push(names);
|
|
466
|
+
});
|
|
467
|
+
return groups;
|
|
468
|
+
}
|
|
469
|
+
export const GARMENT_VISUAL_TAGS_RESOLVE = {
|
|
470
|
+
id: "cp2077.garments.visual-tags-resolve",
|
|
471
|
+
game: "cyberpunk2077",
|
|
472
|
+
groups: ["garments"],
|
|
473
|
+
modTypes: ["garment", "refit", "recolor"],
|
|
474
|
+
summary: "Every hide_*/force_*/HighHeels/FlatShoes tag is documented, or declared in this project's own .xl overrides.tags; component names are unique within an appearance.",
|
|
475
|
+
appliesTo: (t) => {
|
|
476
|
+
const f = fileTarget(t);
|
|
477
|
+
return f !== undefined && (isEntJson(f) || isAppJson(f));
|
|
478
|
+
},
|
|
479
|
+
appliesToDescription: "every .ent.json/.app.json file",
|
|
480
|
+
needs: { project: true },
|
|
481
|
+
cost: "edit",
|
|
482
|
+
severity: "warn",
|
|
483
|
+
evidence: VISUAL_TAGS_EVIDENCE,
|
|
484
|
+
async run(ctx) {
|
|
485
|
+
const file = fileTarget(ctx.target);
|
|
486
|
+
if (!file)
|
|
487
|
+
return [];
|
|
488
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
489
|
+
if (!parsed)
|
|
490
|
+
return [];
|
|
491
|
+
const findings = [];
|
|
492
|
+
const custom = customTagNames((await xlManifests(ctx)));
|
|
493
|
+
for (const hit of collectKey(parsed.root, "tags")) {
|
|
494
|
+
if (!Array.isArray(hit.value))
|
|
495
|
+
continue;
|
|
496
|
+
for (const t of hit.value) {
|
|
497
|
+
const tag = cnameValue(t);
|
|
498
|
+
if (!tag || !tag.startsWith("hide_") && !["HighHeels", "FlatShoes", "force_FlatFeet", "force_Hair", "DynamicAppearance", "AppearanceParts"].includes(tag))
|
|
499
|
+
continue;
|
|
500
|
+
if (custom.has(tag) || KNOWN_VISUAL_TAGS.has(tag))
|
|
501
|
+
continue;
|
|
502
|
+
findings.push({
|
|
503
|
+
validatorId: this.id,
|
|
504
|
+
severity: "warn",
|
|
505
|
+
file,
|
|
506
|
+
message: `Visual tag "${tag}" at ${hit.jsonPath} is not in ModWright's documented vanilla/ArchiveXL tag vocabulary ` +
|
|
507
|
+
"(as of this knowledge base's last check) and is not declared in this project's own .xl overrides.tags block. " +
|
|
508
|
+
"Tags are case-sensitive, so this may be a casing or spelling slip on a real tag.",
|
|
509
|
+
fix: "Check spelling and case against the documented tag list, or declare it as a custom tag in this project's .xl overrides.tags block.",
|
|
510
|
+
evidence: VISUAL_TAGS_EVIDENCE,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const group of collectAppearanceComponentGroups(parsed.root)) {
|
|
515
|
+
const seen = new Map();
|
|
516
|
+
for (const name of group)
|
|
517
|
+
seen.set(name, (seen.get(name) ?? 0) + 1);
|
|
518
|
+
for (const [name, count] of seen) {
|
|
519
|
+
if (count > 1) {
|
|
520
|
+
findings.push({
|
|
521
|
+
validatorId: this.id,
|
|
522
|
+
severity: "block",
|
|
523
|
+
file,
|
|
524
|
+
message: `Component name "${name}" appears ${count} times within one appearance. A custom-tag override in .xl ` +
|
|
525
|
+
"overrides.tags matches on component name alone, so a duplicate silently applies to the wrong component.",
|
|
526
|
+
fix: "Give each component a unique name within the appearance.",
|
|
527
|
+
evidence: VISUAL_TAGS_EVIDENCE,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return findings;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
const ROOT_BLANK_EVIDENCE = ["cp2077.clothing.garments.root-entity-single-blank-appearance"];
|
|
536
|
+
const DYNAMIC_TAG_EVIDENCE = [
|
|
537
|
+
"cp2077.clothing.garments.dynamicappearance-tag-required",
|
|
538
|
+
"cp2077.clothing.garments.dynamic-appearance-placeholders",
|
|
539
|
+
];
|
|
540
|
+
function entRootChunk(root) {
|
|
541
|
+
if (!isJsonObject(root))
|
|
542
|
+
return undefined;
|
|
543
|
+
const data = root.Data;
|
|
544
|
+
if (!isJsonObject(data))
|
|
545
|
+
return undefined;
|
|
546
|
+
const rc = data.RootChunk;
|
|
547
|
+
return isJsonObject(rc) ? rc : undefined;
|
|
548
|
+
}
|
|
549
|
+
function visualTags(rootChunk) {
|
|
550
|
+
const schema = rootChunk.visualTagsSchema;
|
|
551
|
+
if (!isJsonObject(schema))
|
|
552
|
+
return [];
|
|
553
|
+
const data = isJsonObject(schema.Data) ? schema.Data : schema;
|
|
554
|
+
const tagsList = isJsonObject(data) && isJsonObject(data.visualTags) ? data.visualTags.tags : undefined;
|
|
555
|
+
if (!Array.isArray(tagsList))
|
|
556
|
+
return [];
|
|
557
|
+
return tagsList.map((t) => cnameValue(t)).filter((t) => t !== undefined);
|
|
558
|
+
}
|
|
559
|
+
function findBlankAppearanceEntry(rootChunk) {
|
|
560
|
+
const apps = rootChunk.appearances;
|
|
561
|
+
if (!Array.isArray(apps) || apps.length !== 1)
|
|
562
|
+
return undefined;
|
|
563
|
+
const entry = apps[0];
|
|
564
|
+
if (!isJsonObject(entry))
|
|
565
|
+
return undefined;
|
|
566
|
+
return cnameValue(entry.appearanceName) === "" ? entry : undefined;
|
|
567
|
+
}
|
|
568
|
+
function hasSubstitutionComponents(rootChunk) {
|
|
569
|
+
return collectResourcePaths(rootChunk).some(({ path: p }) => p.includes("{"));
|
|
570
|
+
}
|
|
571
|
+
async function siblingEntFiles(ctx, file) {
|
|
572
|
+
const dir = path.dirname(file);
|
|
573
|
+
let entries;
|
|
574
|
+
try {
|
|
575
|
+
entries = await fs.readdir(dir);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
const others = entries.filter((e) => isEntJson(e) && path.join(dir, e) !== file).map((e) => path.join(dir, e));
|
|
581
|
+
others.sort((a, b) => Number(!/root/i.test(path.basename(b))) - Number(!/root/i.test(path.basename(a))));
|
|
582
|
+
return others;
|
|
583
|
+
}
|
|
584
|
+
export const GARMENT_ROOT_ENTITY_BLANK_APPEARANCE = {
|
|
585
|
+
id: "cp2077.garments.root-entity-blank-appearance",
|
|
586
|
+
game: "cyberpunk2077",
|
|
587
|
+
groups: ["garments"],
|
|
588
|
+
modTypes: ["garment", "refit"],
|
|
589
|
+
summary: "A dynamic appearance's root .ent has exactly one appearance entry with a blank appearanceName, and its .app has one entry with components deleted.",
|
|
590
|
+
appliesTo: (t) => {
|
|
591
|
+
const f = fileTarget(t);
|
|
592
|
+
return f !== undefined && isEntJson(f);
|
|
593
|
+
},
|
|
594
|
+
appliesToDescription: "every .ent.json file that carries the DynamicAppearance tag",
|
|
595
|
+
needs: { project: true },
|
|
596
|
+
cost: "edit",
|
|
597
|
+
severity: "block",
|
|
598
|
+
evidence: ROOT_BLANK_EVIDENCE,
|
|
599
|
+
async run(ctx) {
|
|
600
|
+
const file = fileTarget(ctx.target);
|
|
601
|
+
if (!file)
|
|
602
|
+
return [];
|
|
603
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
604
|
+
if (!parsed)
|
|
605
|
+
return [];
|
|
606
|
+
const rootChunk = entRootChunk(parsed.root);
|
|
607
|
+
if (!rootChunk)
|
|
608
|
+
return [];
|
|
609
|
+
if (!visualTags(rootChunk).includes("DynamicAppearance"))
|
|
610
|
+
return [];
|
|
611
|
+
const findings = [];
|
|
612
|
+
const apps = Array.isArray(rootChunk.appearances) ? rootChunk.appearances : [];
|
|
613
|
+
if (apps.length !== 1) {
|
|
614
|
+
findings.push({
|
|
615
|
+
validatorId: this.id,
|
|
616
|
+
severity: "block",
|
|
617
|
+
file,
|
|
618
|
+
message: `This root entity carries DynamicAppearance but its appearances array has ${apps.length} entries, not exactly one.`,
|
|
619
|
+
fix: "A dynamic-appearance root entity keeps exactly one appearances[] entry, with all variant logic in the mesh entity file instead.",
|
|
620
|
+
evidence: ROOT_BLANK_EVIDENCE,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const entry = apps[0];
|
|
625
|
+
const appearanceName = isJsonObject(entry) ? cnameValue(entry.appearanceName) : undefined;
|
|
626
|
+
if (appearanceName !== "") {
|
|
627
|
+
findings.push({
|
|
628
|
+
validatorId: this.id,
|
|
629
|
+
severity: "block",
|
|
630
|
+
file,
|
|
631
|
+
message: `The root entity's single appearance entry has appearanceName "${appearanceName ?? "?"}", not blank.`,
|
|
632
|
+
fix: 'Set appearanceName to "" on the root entity\'s appearance entry; variant selection happens in the .app/mesh entity, not here.',
|
|
633
|
+
evidence: ROOT_BLANK_EVIDENCE,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const appPath = isJsonObject(entry) ? resourcePathValue(entry.appearanceResource) : undefined;
|
|
637
|
+
const root = stagingRoot(ctx.project);
|
|
638
|
+
if (appPath && root) {
|
|
639
|
+
const abs = resolveDepotPath(root, appPath);
|
|
640
|
+
const appJson = await readStagedCr2wJson(ctx, abs);
|
|
641
|
+
if (appJson) {
|
|
642
|
+
const appAppearances = collectKey(appJson.root, "appearances").flatMap((h) => (Array.isArray(h.value) ? h.value : []));
|
|
643
|
+
if (appAppearances.length !== 1) {
|
|
644
|
+
findings.push({
|
|
645
|
+
validatorId: this.id,
|
|
646
|
+
severity: "block",
|
|
647
|
+
file: appJson.absPath,
|
|
648
|
+
related: [file],
|
|
649
|
+
message: `${path.basename(appJson.absPath)} has ${appAppearances.length} appearance entries; a dynamic appearance's .app keeps exactly one.`,
|
|
650
|
+
fix: "Reduce the .app to one appearance entry matching the root entity's appearance name.",
|
|
651
|
+
evidence: ROOT_BLANK_EVIDENCE,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
const def = appAppearances[0];
|
|
656
|
+
const data = isJsonObject(def) && isJsonObject(def.Data) ? def.Data : def;
|
|
657
|
+
const components = isJsonObject(data) ? data.components : undefined;
|
|
658
|
+
if (Array.isArray(components) && components.length > 0) {
|
|
659
|
+
findings.push({
|
|
660
|
+
validatorId: this.id,
|
|
661
|
+
severity: "block",
|
|
662
|
+
file: appJson.absPath,
|
|
663
|
+
related: [file],
|
|
664
|
+
message: `${path.basename(appJson.absPath)}'s single appearance still has ${components.length} component(s); a dynamic appearance's .app has them deleted — all components live in the mesh entity file instead.`,
|
|
665
|
+
fix: "Delete the components array entries from this .app's appearance definition.",
|
|
666
|
+
evidence: ROOT_BLANK_EVIDENCE,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return findings;
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
export const GARMENT_DYNAMICAPPEARANCE_TAG_PRESENT = {
|
|
677
|
+
id: "cp2077.garments.dynamicappearance-tag-present",
|
|
678
|
+
game: "cyberpunk2077",
|
|
679
|
+
groups: ["garments"],
|
|
680
|
+
modTypes: ["garment", "refit"],
|
|
681
|
+
summary: "An entity whose mesh-entity component paths use {…} carries the DynamicAppearance visual tag on its root entity.",
|
|
682
|
+
appliesTo: (t) => {
|
|
683
|
+
const f = fileTarget(t);
|
|
684
|
+
return f !== undefined && isEntJson(f);
|
|
685
|
+
},
|
|
686
|
+
appliesToDescription: "every .ent.json file with at least one {…}-substituted component depotPath",
|
|
687
|
+
needs: { project: true },
|
|
688
|
+
cost: "edit",
|
|
689
|
+
severity: "block",
|
|
690
|
+
evidence: DYNAMIC_TAG_EVIDENCE,
|
|
691
|
+
async run(ctx) {
|
|
692
|
+
const file = fileTarget(ctx.target);
|
|
693
|
+
if (!file)
|
|
694
|
+
return [];
|
|
695
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
696
|
+
if (!parsed)
|
|
697
|
+
return [];
|
|
698
|
+
const rootChunk = entRootChunk(parsed.root);
|
|
699
|
+
if (!rootChunk || !hasSubstitutionComponents(rootChunk))
|
|
700
|
+
return [];
|
|
701
|
+
for (const sibling of await siblingEntFiles(ctx, file)) {
|
|
702
|
+
const siblingParsed = await readCr2wJson(ctx, sibling);
|
|
703
|
+
if (!siblingParsed)
|
|
704
|
+
continue;
|
|
705
|
+
const siblingChunk = entRootChunk(siblingParsed.root);
|
|
706
|
+
const looksLikeRoot = siblingChunk && (findBlankAppearanceEntry(siblingChunk) !== undefined || /root/i.test(path.basename(sibling)));
|
|
707
|
+
if (!siblingChunk || !looksLikeRoot)
|
|
708
|
+
continue;
|
|
709
|
+
if (visualTags(siblingChunk).includes("DynamicAppearance"))
|
|
710
|
+
return [];
|
|
711
|
+
return [
|
|
712
|
+
{
|
|
713
|
+
validatorId: "cp2077.garments.dynamicappearance-tag-present",
|
|
714
|
+
severity: "block",
|
|
715
|
+
file: sibling,
|
|
716
|
+
related: [file],
|
|
717
|
+
message: `${path.basename(file)} substitutes {…} placeholders in a component depotPath, but its paired root entity ` +
|
|
718
|
+
`${path.basename(sibling)} does not carry the DynamicAppearance visual tag.`,
|
|
719
|
+
fix: "Add DynamicAppearance to the root entity's visualTagsSchema.Data.visualTags.tags.",
|
|
720
|
+
evidence: DYNAMIC_TAG_EVIDENCE,
|
|
721
|
+
},
|
|
722
|
+
];
|
|
723
|
+
}
|
|
724
|
+
return [];
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
const GENDER_SUFFIX_EVIDENCE = [
|
|
728
|
+
"cp2077.clothing.garments.gendered-appearance-name-suffix",
|
|
729
|
+
"cp2077.clothing.garments.unvalidated-variant-crashes",
|
|
730
|
+
];
|
|
731
|
+
export const GARMENT_GENDERED_APPEARANCE_SUFFIX = {
|
|
732
|
+
id: "cp2077.garments.gendered-appearance-suffix",
|
|
733
|
+
game: "cyberpunk2077",
|
|
734
|
+
groups: ["garments"],
|
|
735
|
+
modTypes: ["garment", "refit"],
|
|
736
|
+
summary: "When only one gender's meshes are staged, every .app appearance name carries &gender=w or &gender=m.",
|
|
737
|
+
appliesTo: (t) => t.kind === "project",
|
|
738
|
+
appliesToDescription: "once per validation, when a {gender}-substituted component is staged for only one gender",
|
|
739
|
+
needs: { project: true },
|
|
740
|
+
cost: "build",
|
|
741
|
+
severity: "block",
|
|
742
|
+
evidence: GENDER_SUFFIX_EVIDENCE,
|
|
743
|
+
async run(ctx) {
|
|
744
|
+
const root = stagingRoot(ctx.project);
|
|
745
|
+
if (!root)
|
|
746
|
+
return [];
|
|
747
|
+
const manifests = await xlManifests(ctx);
|
|
748
|
+
const values = { ...ENUMERABLE_VALUES, body: bodyValueSet(manifests) };
|
|
749
|
+
const entFiles = await allFilesUnder(ctx.project, isEntJson);
|
|
750
|
+
const findings = [];
|
|
751
|
+
const checkedDirs = new Set();
|
|
752
|
+
for (const file of entFiles) {
|
|
753
|
+
const parsed = await readCr2wJson(ctx, file);
|
|
754
|
+
if (!parsed)
|
|
755
|
+
continue;
|
|
756
|
+
const genderPaths = collectResourcePaths(parsed.root).filter(({ path: p }) => p.startsWith("*") && placeholdersIn(p).includes("gender"));
|
|
757
|
+
if (genderPaths.length === 0)
|
|
758
|
+
continue;
|
|
759
|
+
const stagedGenders = new Set();
|
|
760
|
+
for (const { path: p } of genderPaths) {
|
|
761
|
+
const tokens = [...new Set(placeholdersIn(p))].filter((t) => t in values);
|
|
762
|
+
if (tokens.length === 0)
|
|
763
|
+
continue;
|
|
764
|
+
const domains = tokens.map((t) => values[t]);
|
|
765
|
+
let combos = [[]];
|
|
766
|
+
for (const domain of domains)
|
|
767
|
+
combos = combos.flatMap((prefix) => domain.map((v) => [...prefix, v]));
|
|
768
|
+
for (const combo of combos) {
|
|
769
|
+
let resolved = p;
|
|
770
|
+
tokens.forEach((t, i) => (resolved = resolved.split(`{${t}}`).join(combo[i])));
|
|
771
|
+
if (await ctx.exists(resolveDepotPath(root, resolved))) {
|
|
772
|
+
stagedGenders.add(combo[tokens.indexOf("gender")]);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (stagedGenders.size !== 1)
|
|
777
|
+
continue;
|
|
778
|
+
const [onlyGender] = [...stagedGenders];
|
|
779
|
+
const dir = path.dirname(file);
|
|
780
|
+
if (checkedDirs.has(dir))
|
|
781
|
+
continue;
|
|
782
|
+
checkedDirs.add(dir);
|
|
783
|
+
let entries;
|
|
784
|
+
try {
|
|
785
|
+
entries = await fs.readdir(dir);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
for (const entry of entries.filter(isAppJson)) {
|
|
791
|
+
const appFile = path.join(dir, entry);
|
|
792
|
+
const appJson = await readCr2wJson(ctx, appFile);
|
|
793
|
+
if (!appJson)
|
|
794
|
+
continue;
|
|
795
|
+
for (const hit of collectKey(appJson.root, "name")) {
|
|
796
|
+
const name = cnameValue(hit.value);
|
|
797
|
+
if (!name)
|
|
798
|
+
continue;
|
|
799
|
+
if (!name.includes(`&gender=${onlyGender}`)) {
|
|
800
|
+
findings.push({
|
|
801
|
+
validatorId: this.id,
|
|
802
|
+
severity: "block",
|
|
803
|
+
file: appFile,
|
|
804
|
+
related: [file],
|
|
805
|
+
message: `Only the "${onlyGender}" gender's meshes are staged for the component(s) in ${path.basename(file)}, ` +
|
|
806
|
+
`but appearance name "${name}" at ${hit.jsonPath} does not carry "&gender=${onlyGender}". ` +
|
|
807
|
+
"The game crashes if the item is equipped by the unsupported body gender.",
|
|
808
|
+
fix: `Append "&gender=${onlyGender}" to this appearance's name.`,
|
|
809
|
+
evidence: GENDER_SUFFIX_EVIDENCE,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return findings;
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
const XL_ENTRIES_EVIDENCE = [
|
|
819
|
+
"cp2077.archivexl.manifest-top-level-keys",
|
|
820
|
+
"cp2077.archivexl.factories-csv-paths",
|
|
821
|
+
"cp2077.archivexl.streaming-blocks-path",
|
|
822
|
+
"cp2077.world.loot-container.three-artifacts-must-deploy-together",
|
|
823
|
+
];
|
|
824
|
+
async function stagingLooksPartial(project) {
|
|
825
|
+
const root = stagingRoot(project);
|
|
826
|
+
if (!root)
|
|
827
|
+
return true;
|
|
828
|
+
const walked = await walk(root, { limit: 1000 });
|
|
829
|
+
const payload = walked.files.filter((f) => !isXlManifest(f));
|
|
830
|
+
return payload.length === 0;
|
|
831
|
+
}
|
|
832
|
+
function collectManifestPaths(manifest) {
|
|
833
|
+
const out = [];
|
|
834
|
+
const localization = manifest.localization;
|
|
835
|
+
if (isJsonObject(localization) && isJsonObject(localization.onscreens)) {
|
|
836
|
+
for (const [lang, value] of Object.entries(localization.onscreens)) {
|
|
837
|
+
if (typeof value === "string")
|
|
838
|
+
out.push({ label: `localization.onscreens.${lang}`, value });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (Array.isArray(manifest.factories)) {
|
|
842
|
+
manifest.factories.forEach((v, i) => {
|
|
843
|
+
if (typeof v === "string")
|
|
844
|
+
out.push({ label: `factories[${i}]`, value: v });
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const streaming = manifest.streaming;
|
|
848
|
+
if (isJsonObject(streaming) && Array.isArray(streaming.blocks)) {
|
|
849
|
+
streaming.blocks.forEach((v, i) => {
|
|
850
|
+
if (typeof v === "string")
|
|
851
|
+
out.push({ label: `streaming.blocks[${i}]`, value: v });
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (Array.isArray(manifest.animations)) {
|
|
855
|
+
manifest.animations.forEach((entry, i) => {
|
|
856
|
+
if (!isJsonObject(entry))
|
|
857
|
+
return;
|
|
858
|
+
if (typeof entry.set === "string")
|
|
859
|
+
out.push({ label: `animations[${i}].set`, value: entry.set });
|
|
860
|
+
if (typeof entry.entity === "string")
|
|
861
|
+
out.push({ label: `animations[${i}].entity`, value: entry.entity });
|
|
862
|
+
if (Array.isArray(entry.entity)) {
|
|
863
|
+
entry.entity.forEach((v, j) => {
|
|
864
|
+
if (typeof v === "string")
|
|
865
|
+
out.push({ label: `animations[${i}].entity[${j}]`, value: v });
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
const resource = manifest.resource;
|
|
871
|
+
if (isJsonObject(resource) && isJsonObject(resource.patch)) {
|
|
872
|
+
for (const key of Object.keys(resource.patch))
|
|
873
|
+
out.push({ label: `resource.patch["${key}"]`, value: key });
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
export const GARMENT_XL_ENTRIES_RESOLVE = {
|
|
878
|
+
id: "cp2077.garments.xl-entries-resolve",
|
|
879
|
+
game: "cyberpunk2077",
|
|
880
|
+
groups: ["garments"],
|
|
881
|
+
modTypes: ["garment", "refit", "outfit-slot"],
|
|
882
|
+
summary: "Every path an .xl manifest declares resolves to a file staged under the archive-staging source or the vanilla archive index.",
|
|
883
|
+
appliesTo: (t) => t.kind === "project",
|
|
884
|
+
appliesToDescription: "once per validation, over every .archive.xl manifest in the project",
|
|
885
|
+
needs: { project: true },
|
|
886
|
+
cost: "build",
|
|
887
|
+
severity: "block",
|
|
888
|
+
evidence: XL_ENTRIES_EVIDENCE,
|
|
889
|
+
async run(ctx) {
|
|
890
|
+
const manifests = await xlManifests(ctx);
|
|
891
|
+
if (manifests.length === 0)
|
|
892
|
+
return [];
|
|
893
|
+
if (await stagingLooksPartial(ctx.project)) {
|
|
894
|
+
return [
|
|
895
|
+
{
|
|
896
|
+
validatorId: this.id,
|
|
897
|
+
severity: "info",
|
|
898
|
+
file: manifests[0].file,
|
|
899
|
+
message: "The project's archive-staging source is missing or stages nothing beyond the manifest itself; " +
|
|
900
|
+
"cp2077.garments.xl-entries-resolve cannot tell a genuinely missing entry from a fixture/partial tree and was not run. " +
|
|
901
|
+
"Run this against the built archive-staging output.",
|
|
902
|
+
evidence: XL_ENTRIES_EVIDENCE,
|
|
903
|
+
},
|
|
904
|
+
];
|
|
905
|
+
}
|
|
906
|
+
const root = stagingRoot(ctx.project);
|
|
907
|
+
const findings = [];
|
|
908
|
+
for (const { file, manifest } of manifests) {
|
|
909
|
+
for (const { label, value } of collectManifestPaths(manifest)) {
|
|
910
|
+
const abs = resolveDepotPath(root, value);
|
|
911
|
+
if (!(await existsStagedOrVanilla(ctx, abs, value))) {
|
|
912
|
+
findings.push({
|
|
913
|
+
validatorId: this.id,
|
|
914
|
+
severity: "block",
|
|
915
|
+
file,
|
|
916
|
+
message: `${label} = "${value}" does not resolve under the staged archive tree (expected at ${abs}) or the vanilla archive index.`,
|
|
917
|
+
fix: `Stage the file at "${value}", or fix the path in ${path.basename(file)}.`,
|
|
918
|
+
evidence: XL_ENTRIES_EVIDENCE,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return findings;
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
const FACTORY_CSV_EVIDENCE = [
|
|
927
|
+
"cp2077.archivexl.factory-csv-appearance-mapping",
|
|
928
|
+
"cp2077.entities.entity-appearance-factory-resolution",
|
|
929
|
+
"cp2077.tweakdb.weapon-visuals-via-appearanceresourcename",
|
|
930
|
+
];
|
|
931
|
+
function parseCsvRows(text) {
|
|
932
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
933
|
+
if (lines.length === 0)
|
|
934
|
+
return [];
|
|
935
|
+
const header = lines[0].split(",").map((h) => h.trim());
|
|
936
|
+
return lines.slice(1).map((line) => {
|
|
937
|
+
const cells = line.split(",");
|
|
938
|
+
const row = {};
|
|
939
|
+
header.forEach((h, i) => (row[h] = (cells[i] ?? "").trim()));
|
|
940
|
+
return row;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
async function appearanceResourceNames(ctx) {
|
|
944
|
+
const files = await allFilesUnder(ctx.project, isYamlFile);
|
|
945
|
+
const out = [];
|
|
946
|
+
for (const file of files) {
|
|
947
|
+
let text;
|
|
948
|
+
try {
|
|
949
|
+
text = await ctx.readText(file);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
let records;
|
|
955
|
+
try {
|
|
956
|
+
records = parseTweakXLYaml(text, file);
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
for (const rec of records) {
|
|
962
|
+
const flat = rec.flats.find((f) => f.name === "appearanceResourceName");
|
|
963
|
+
if (flat && typeof flat.value === "string")
|
|
964
|
+
out.push({ name: flat.value, file });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
export const GARMENT_FACTORY_CSV_ROWS = {
|
|
970
|
+
id: "cp2077.garments.factory-csv-rows",
|
|
971
|
+
game: "cyberpunk2077",
|
|
972
|
+
groups: ["garments"],
|
|
973
|
+
modTypes: ["garment", "refit"],
|
|
974
|
+
summary: "Every appearance name a TweakXL item record names has a row in a registered factory CSV, that row's path is staged or resolves in the vanilla archive index, and a row no record names is reported.",
|
|
975
|
+
appliesTo: (t) => t.kind === "project",
|
|
976
|
+
appliesToDescription: "once per validation, over appearanceResourceName values against the .xl's registered factory CSVs",
|
|
977
|
+
needs: { project: true, toolchain: ["wolvenkit-cli"] },
|
|
978
|
+
cost: "build",
|
|
979
|
+
severity: "block",
|
|
980
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
981
|
+
async run(ctx) {
|
|
982
|
+
const names = await appearanceResourceNames(ctx);
|
|
983
|
+
if (names.length === 0)
|
|
984
|
+
return [];
|
|
985
|
+
const root = stagingRoot(ctx.project);
|
|
986
|
+
const manifests = await xlManifests(ctx);
|
|
987
|
+
const csvPaths = manifests.flatMap(({ manifest }) => (Array.isArray(manifest.factories) ? manifest.factories.filter((f) => typeof f === "string") : []));
|
|
988
|
+
if (csvPaths.length === 0 || !root)
|
|
989
|
+
return [];
|
|
990
|
+
const findings = [];
|
|
991
|
+
const rows = [];
|
|
992
|
+
let anyCsvReadable = false;
|
|
993
|
+
for (const csvPath of csvPaths) {
|
|
994
|
+
const abs = resolveDepotPath(root, csvPath);
|
|
995
|
+
let text;
|
|
996
|
+
try {
|
|
997
|
+
text = await ctx.readText(abs);
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
text = undefined;
|
|
1001
|
+
}
|
|
1002
|
+
if (text !== undefined && /^\s*name\s*,\s*path/i.test(text)) {
|
|
1003
|
+
rows.push(...parseCsvRows(text).map((cells) => ({ csv: abs, cells })));
|
|
1004
|
+
anyCsvReadable = true;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const jsonSidecar = `${abs}.json`;
|
|
1008
|
+
let jsonText;
|
|
1009
|
+
try {
|
|
1010
|
+
jsonText = await ctx.readText(jsonSidecar);
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
jsonText = undefined;
|
|
1014
|
+
}
|
|
1015
|
+
if (jsonText !== undefined) {
|
|
1016
|
+
anyCsvReadable = true;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (await ctx.exists(abs)) {
|
|
1020
|
+
const wolvenkitSpec = TOOL_SPECS.find((s) => s.id === "wolvenkit-cli");
|
|
1021
|
+
const wolvenkit = wolvenkitSpec ? await locateTool(wolvenkitSpec) : undefined;
|
|
1022
|
+
if (!wolvenkit || !wolvenkit.found) {
|
|
1023
|
+
findings.push({
|
|
1024
|
+
validatorId: this.id,
|
|
1025
|
+
severity: "info",
|
|
1026
|
+
file: abs,
|
|
1027
|
+
message: `${csvPath} is a compiled CR2W factory CSV with no text or .json intermediate on disk, and wolvenkit-cli was not located to serialize it; cp2077.garments.factory-csv-rows was not run for this CSV.`,
|
|
1028
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1029
|
+
});
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const result = await runTool(wolvenkit, {
|
|
1033
|
+
args: ["convert", "serialize", "-p", abs],
|
|
1034
|
+
timeoutMs: 60_000,
|
|
1035
|
+
});
|
|
1036
|
+
if (!result.ok) {
|
|
1037
|
+
findings.push({
|
|
1038
|
+
validatorId: this.id,
|
|
1039
|
+
severity: "info",
|
|
1040
|
+
file: abs,
|
|
1041
|
+
message: `wolvenkit-cli convert serialize failed on ${csvPath} (${result.error?.message ?? "unknown error"}); cp2077.garments.factory-csv-rows was not run for this CSV.`,
|
|
1042
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (!anyCsvReadable) {
|
|
1049
|
+
return [
|
|
1050
|
+
{
|
|
1051
|
+
validatorId: this.id,
|
|
1052
|
+
severity: "info",
|
|
1053
|
+
file: manifests[0].file,
|
|
1054
|
+
message: "None of this project's registered factory CSVs could be read as text or json (fixture/partial tree, or a compiled " +
|
|
1055
|
+
"binary this check cannot parse); cp2077.garments.factory-csv-rows was not run.",
|
|
1056
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1057
|
+
},
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
for (const { name, file } of names) {
|
|
1061
|
+
const row = rows.find((r) => r.cells.name === name)?.cells;
|
|
1062
|
+
if (!row) {
|
|
1063
|
+
findings.push({
|
|
1064
|
+
validatorId: this.id,
|
|
1065
|
+
severity: "block",
|
|
1066
|
+
file,
|
|
1067
|
+
message: `appearanceResourceName "${name}" has no row in any registered factory CSV.`,
|
|
1068
|
+
fix: `Add a row for "${name}" to the factory CSV registered in this project's .xl.`,
|
|
1069
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1070
|
+
});
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const rowPath = row.path;
|
|
1074
|
+
if (!rowPath)
|
|
1075
|
+
continue;
|
|
1076
|
+
const abs = resolveDepotPath(root, rowPath);
|
|
1077
|
+
if (!(await existsStagedOrVanilla(ctx, abs, rowPath))) {
|
|
1078
|
+
findings.push({
|
|
1079
|
+
validatorId: this.id,
|
|
1080
|
+
severity: "block",
|
|
1081
|
+
file,
|
|
1082
|
+
message: `Factory row "${name}" points at "${rowPath}", which is not staged (expected at ${abs}) or in the vanilla archive index.`,
|
|
1083
|
+
fix: `Stage the .app/.ent at "${rowPath}", or fix the factory row's path.`,
|
|
1084
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const named = new Set(names.map((n) => n.name));
|
|
1089
|
+
for (const { csv, cells } of rows) {
|
|
1090
|
+
if (!cells.name || named.has(cells.name))
|
|
1091
|
+
continue;
|
|
1092
|
+
findings.push({
|
|
1093
|
+
validatorId: this.id,
|
|
1094
|
+
severity: "info",
|
|
1095
|
+
file: csv,
|
|
1096
|
+
message: `Factory row "${cells.name}" is named by no TweakXL record's appearanceResourceName in this project's yaml, so nothing here uses it and its path ("${cells.path ?? ""}") is not checked.`,
|
|
1097
|
+
fix: `Set appearanceResourceName: ${cells.name} on the record that should use it, or remove the row.`,
|
|
1098
|
+
evidence: FACTORY_CSV_EVIDENCE,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
return findings;
|
|
1102
|
+
},
|
|
1103
|
+
};
|
|
1104
|
+
const EQUIPMENTEX_EVIDENCE = [
|
|
1105
|
+
"cp2077.equipment-ex.slots.torso-layer-names",
|
|
1106
|
+
"cp2077.equipment-ex.slots.legs-and-body-layer-names",
|
|
1107
|
+
"cp2077.equipment-ex.slots.paired-left-right-naming",
|
|
1108
|
+
"cp2077.equipment-ex.slots.placementslots-append-syntax",
|
|
1109
|
+
"cp2077.equipment-ex.slots.auto-conversion-table",
|
|
1110
|
+
];
|
|
1111
|
+
export const GARMENT_EQUIPMENTEX_SLOT_KNOWN = {
|
|
1112
|
+
id: "cp2077.garments.equipmentex-slot-known",
|
|
1113
|
+
game: "cyberpunk2077",
|
|
1114
|
+
groups: ["garments"],
|
|
1115
|
+
modTypes: ["garment", "outfit-slot"],
|
|
1116
|
+
summary: "placementSlots entries naming OutfitSlots.* are known Equipment-EX slot ids.",
|
|
1117
|
+
appliesTo: (t) => {
|
|
1118
|
+
const f = fileTarget(t);
|
|
1119
|
+
return f !== undefined && isYamlFile(f);
|
|
1120
|
+
},
|
|
1121
|
+
appliesToDescription: "every .yaml/.yml file",
|
|
1122
|
+
needs: {},
|
|
1123
|
+
cost: "edit",
|
|
1124
|
+
severity: "warn",
|
|
1125
|
+
evidence: EQUIPMENTEX_EVIDENCE,
|
|
1126
|
+
async run(ctx) {
|
|
1127
|
+
const file = fileTarget(ctx.target);
|
|
1128
|
+
if (!file)
|
|
1129
|
+
return [];
|
|
1130
|
+
let text;
|
|
1131
|
+
try {
|
|
1132
|
+
text = await ctx.readText(file);
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
return [];
|
|
1136
|
+
}
|
|
1137
|
+
let records;
|
|
1138
|
+
try {
|
|
1139
|
+
records = parseTweakXLYaml(text, file);
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
const findings = [];
|
|
1145
|
+
for (const rec of records) {
|
|
1146
|
+
const flat = rec.flats.find((f) => f.name === "placementSlots");
|
|
1147
|
+
if (!flat || !Array.isArray(flat.value))
|
|
1148
|
+
continue;
|
|
1149
|
+
for (const v of flat.value) {
|
|
1150
|
+
if (typeof v !== "string" || !v.startsWith("OutfitSlots."))
|
|
1151
|
+
continue;
|
|
1152
|
+
const slot = v.slice("OutfitSlots.".length);
|
|
1153
|
+
if (KNOWN_OUTFIT_SLOTS.has(slot))
|
|
1154
|
+
continue;
|
|
1155
|
+
findings.push({
|
|
1156
|
+
validatorId: this.id,
|
|
1157
|
+
severity: "warn",
|
|
1158
|
+
file,
|
|
1159
|
+
line: rec.line,
|
|
1160
|
+
message: `${rec.name}'s placementSlots names "${v}", which is not a documented Equipment-EX outfit slot id.`,
|
|
1161
|
+
fix: `Check the slot name against Equipment-EX's outfit-slot table (e.g. "OutfitSlots.TorsoMiddle", not "${v}").`,
|
|
1162
|
+
evidence: EQUIPMENTEX_EVIDENCE,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return findings;
|
|
1167
|
+
},
|
|
1168
|
+
};
|
|
1169
|
+
export const GARMENT_VALIDATORS = [
|
|
1170
|
+
GARMENT_STAR_PREFIX,
|
|
1171
|
+
GARMENT_PLACEHOLDER_KNOWN,
|
|
1172
|
+
GARMENT_COMBINATION_RESOLVES,
|
|
1173
|
+
GARMENT_BODY_TAG_REGISTRATION,
|
|
1174
|
+
GARMENT_VISUAL_TAGS_RESOLVE,
|
|
1175
|
+
GARMENT_ROOT_ENTITY_BLANK_APPEARANCE,
|
|
1176
|
+
GARMENT_DYNAMICAPPEARANCE_TAG_PRESENT,
|
|
1177
|
+
GARMENT_GENDERED_APPEARANCE_SUFFIX,
|
|
1178
|
+
GARMENT_XL_ENTRIES_RESOLVE,
|
|
1179
|
+
GARMENT_FACTORY_CSV_ROWS,
|
|
1180
|
+
GARMENT_EQUIPMENTEX_SLOT_KNOWN,
|
|
1181
|
+
];
|