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,308 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { compareVersions } from "../../../core/compat/version.js";
|
|
3
|
+
const ID = "cp2077.history.xl-manifest-lint";
|
|
4
|
+
const GAME = "cyberpunk2077";
|
|
5
|
+
const GROUPS = ["history"];
|
|
6
|
+
const MOD_TYPES = ["garment", "vehicle", "packaging"];
|
|
7
|
+
const SEVERITY = "warn";
|
|
8
|
+
const EVIDENCE = [
|
|
9
|
+
"cp2077.archivexl.manifest-top-level-keys",
|
|
10
|
+
"cp2077.anims.archivexl.animconfig-vars-parser-bug",
|
|
11
|
+
"cp2077.anims.archivexl.animconfig-entity-list-parser-bug",
|
|
12
|
+
"cp2077.anims.archivexl.pr-merged-upstream",
|
|
13
|
+
];
|
|
14
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set(["localization", "factories", "streaming", "resource", "animations", "player", "quest"]);
|
|
15
|
+
const QUEST_EVIDENCE = ["cp2077.quests.scenes.archivexl-quest-phase-merge"];
|
|
16
|
+
const PLAYER_EVIDENCE = ["cp2077.archivexl.bodytypes.player-bodytypes-key"];
|
|
17
|
+
const ARCHIVEXL_ANIM_FIX_VERSION = "1.27.3";
|
|
18
|
+
const KNOWN_BUGGY_ARCHIVEXL_VERSIONS = new Set(["1.27.0", "1.27.1", "1.28.0-beta.1"]);
|
|
19
|
+
function isArchiveXLAnimConfigBuggy(version) {
|
|
20
|
+
const v = version.trim();
|
|
21
|
+
if (KNOWN_BUGGY_ARCHIVEXL_VERSIONS.has(v))
|
|
22
|
+
return true;
|
|
23
|
+
try {
|
|
24
|
+
return compareVersions(v, ARCHIVEXL_ANIM_FIX_VERSION) < 0;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function extensionOf(p) {
|
|
31
|
+
const idx = p.lastIndexOf(".");
|
|
32
|
+
return idx === -1 ? "" : p.slice(idx).toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function normalizeSlashes(p) {
|
|
35
|
+
return p.replace(/\\/g, "/").toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
const PLAYER_FPP_TARGET_RE = /player_[mw]a_fpp(?:_ep1)?\.ent$/i;
|
|
38
|
+
function isRecord(v) {
|
|
39
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
40
|
+
}
|
|
41
|
+
function isStringArray(v) {
|
|
42
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
43
|
+
}
|
|
44
|
+
function finding(file, message, evidence, fix) {
|
|
45
|
+
return { validatorId: ID, severity: SEVERITY, file, message, fix, evidence };
|
|
46
|
+
}
|
|
47
|
+
function lintLocalization(doc, file, out) {
|
|
48
|
+
const localization = doc.localization;
|
|
49
|
+
if (localization === undefined)
|
|
50
|
+
return;
|
|
51
|
+
if (!isRecord(localization)) {
|
|
52
|
+
out.push(finding(file, `"localization:" must be a mapping with an "onscreens" key.`, EVIDENCE));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const onscreens = localization.onscreens;
|
|
56
|
+
if (onscreens === undefined) {
|
|
57
|
+
out.push(finding(file, `"localization:" has no "onscreens" key. The documented shape maps a language code (e.g. en-us) to a localization .json path.`, EVIDENCE));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(onscreens)) {
|
|
61
|
+
out.push(finding(file, `"localization: onscreens:" must be a mapping of language code -> .json path.`, EVIDENCE));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const [lang, value] of Object.entries(onscreens)) {
|
|
65
|
+
if (typeof value !== "string" || !normalizeSlashes(value).endsWith(".json")) {
|
|
66
|
+
out.push(finding(file, `"localization: onscreens: ${lang}:" is ${JSON.stringify(value)}, which does not look like a .json path.`, EVIDENCE));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function lintPlayer(doc, file, out) {
|
|
71
|
+
const player = doc.player;
|
|
72
|
+
if (player === undefined)
|
|
73
|
+
return;
|
|
74
|
+
if (!isRecord(player)) {
|
|
75
|
+
out.push(finding(file, `"player:" must be a mapping with a "bodyTypes" key.`, PLAYER_EVIDENCE));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const bodyTypes = player.bodyTypes;
|
|
79
|
+
if (bodyTypes === undefined) {
|
|
80
|
+
out.push(finding(file, `"player:" has no "bodyTypes" key (a non-empty list of body-tag names registered via ArchiveXL's player.bodyTypes).`, PLAYER_EVIDENCE));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isStringArray(bodyTypes) || bodyTypes.length === 0) {
|
|
84
|
+
out.push(finding(file, `"player: bodyTypes:" must be a non-empty list of body-tag name strings.`, PLAYER_EVIDENCE));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function lintFactories(doc, file, out) {
|
|
88
|
+
const factories = doc.factories;
|
|
89
|
+
if (factories === undefined)
|
|
90
|
+
return;
|
|
91
|
+
if (!Array.isArray(factories)) {
|
|
92
|
+
out.push(finding(file, `"factories:" must be a list of factory .csv paths.`, EVIDENCE));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const entry of factories) {
|
|
96
|
+
if (typeof entry !== "string" || !normalizeSlashes(entry).endsWith(".csv")) {
|
|
97
|
+
out.push(finding(file, `"factories:" entry ${JSON.stringify(entry)} does not look like a .csv path. Every factories entry registers a factory csv resource (cp2077.archivexl.factories-csv-paths).`, EVIDENCE));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function lintStreaming(doc, file, out) {
|
|
102
|
+
const streaming = doc.streaming;
|
|
103
|
+
if (streaming === undefined)
|
|
104
|
+
return;
|
|
105
|
+
if (!isRecord(streaming)) {
|
|
106
|
+
out.push(finding(file, `"streaming:" must be a mapping with a "blocks" key.`, EVIDENCE));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const blocks = streaming.blocks;
|
|
110
|
+
if (blocks === undefined) {
|
|
111
|
+
out.push(finding(file, `"streaming:" has no "blocks" key (a list of .streamingblock paths).`, EVIDENCE));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(blocks)) {
|
|
115
|
+
out.push(finding(file, `"streaming: blocks:" must be a list of .streamingblock paths.`, EVIDENCE));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const entry of blocks) {
|
|
119
|
+
if (typeof entry !== "string" || !normalizeSlashes(entry).endsWith(".streamingblock")) {
|
|
120
|
+
out.push(finding(file, `"streaming: blocks:" entry ${JSON.stringify(entry)} does not look like a .streamingblock path.`, EVIDENCE));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function lintQuestPhases(doc, file, out) {
|
|
125
|
+
const quest = doc.quest;
|
|
126
|
+
if (quest === undefined)
|
|
127
|
+
return;
|
|
128
|
+
if (!isRecord(quest) || !Array.isArray(quest.phases)) {
|
|
129
|
+
out.push(finding(file, `"quest:" must be a mapping with a "phases" list of { path, parent } entries.`, QUEST_EVIDENCE));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
quest.phases.forEach((entry, i) => {
|
|
133
|
+
if (!isRecord(entry)) {
|
|
134
|
+
out.push(finding(file, `"quest: phases:" entry ${i + 1} must be a mapping with "path" and "parent".`, QUEST_EVIDENCE));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const phasePath = typeof entry.path === "string" ? normalizeSlashes(entry.path) : "";
|
|
138
|
+
if (!phasePath.endsWith(".questphase")) {
|
|
139
|
+
out.push(finding(file, `"quest: phases:" entry ${i + 1} has path ${JSON.stringify(entry.path)}, which does not look like a .questphase path.`, QUEST_EVIDENCE));
|
|
140
|
+
}
|
|
141
|
+
const parent = typeof entry.parent === "string" ? normalizeSlashes(entry.parent) : "";
|
|
142
|
+
if (!parent.endsWith(".questphase") && !parent.endsWith(".quest")) {
|
|
143
|
+
out.push(finding(file, entry.parent === undefined
|
|
144
|
+
? `"quest: phases:" entry ${i + 1} has no "parent" (the .quest or .questphase to merge into).`
|
|
145
|
+
: `"quest: phases:" entry ${i + 1} has parent ${JSON.stringify(entry.parent)}, which is neither a .questphase nor a .quest.`, QUEST_EVIDENCE));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function lintResourcePatch(doc, file, out) {
|
|
150
|
+
const resource = doc.resource;
|
|
151
|
+
if (resource === undefined)
|
|
152
|
+
return;
|
|
153
|
+
if (!isRecord(resource)) {
|
|
154
|
+
out.push(finding(file, `"resource:" must be a mapping with a "patch" key.`, EVIDENCE));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const patch = resource.patch;
|
|
158
|
+
if (patch === undefined)
|
|
159
|
+
return;
|
|
160
|
+
if (!isRecord(patch)) {
|
|
161
|
+
out.push(finding(file, `"resource: patch:" must map a patch .ent path to a list of target .ent paths.`, EVIDENCE));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const [patchKey, targets] of Object.entries(patch)) {
|
|
165
|
+
if (!normalizeSlashes(patchKey).endsWith(".ent")) {
|
|
166
|
+
out.push(finding(file, `"resource: patch:" key ${JSON.stringify(patchKey)} does not look like a patch .ent path.`, EVIDENCE));
|
|
167
|
+
}
|
|
168
|
+
if (!isStringArray(targets)) {
|
|
169
|
+
out.push(finding(file, `"resource: patch: ${patchKey}:" must be a list of target .ent paths.`, EVIDENCE));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (targets.length === 0) {
|
|
173
|
+
out.push(finding(file, `"resource: patch: ${patchKey}:" lists no target .ent path. ArchiveXL rejects the entry ("Patch definition must have at least one target") and the patch .ent never attaches to anything.`, ["cp2077.archivexl.bodytypes.resource-patch-entry-needs-at-least-one-target"], `List the vanilla .ent path(s) this patch applies to (for a player body patch: the base\\ and ep1\\ player FPP entities per gender).`));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const target of targets) {
|
|
177
|
+
if (!normalizeSlashes(target).endsWith(".ent")) {
|
|
178
|
+
out.push(finding(file, `"resource: patch: ${patchKey}:" target ${JSON.stringify(target)} does not look like an .ent path.`, EVIDENCE));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const isPlayerFpp = targets.some((t) => PLAYER_FPP_TARGET_RE.test(normalizeSlashes(t)));
|
|
182
|
+
if (isPlayerFpp) {
|
|
183
|
+
const hasBase = targets.some((t) => normalizeSlashes(t).startsWith("base/"));
|
|
184
|
+
const hasEp1 = targets.some((t) => normalizeSlashes(t).startsWith("ep1/"));
|
|
185
|
+
if (!hasBase || !hasEp1) {
|
|
186
|
+
const missing = !hasBase ? "base\\" : "ep1\\";
|
|
187
|
+
const consequence = !hasBase
|
|
188
|
+
? "silently no-ops with no log line when Phantom Liberty is NOT installed (the ep1 template is a standalone copy, not an include; base\\ is the no-EP1 fallback)"
|
|
189
|
+
: "silently no-ops with no log line when Phantom Liberty IS installed (the game loads the ep1\\ template instead of base\\)";
|
|
190
|
+
out.push(finding(file, `"resource: patch: ${patchKey}:" patches a player FPP entity but lists no ${missing} target. It ${consequence}. List both the base\\ and ep1\\ (both genders, as needed) template paths in the same target list.`, ["cp2077.anims.sets.patch-the-ep1-player-ent"], `Add the missing ${missing} target path to this patch entry's target list.`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function lintAnimations(doc, file, ctx, out) {
|
|
196
|
+
const animations = doc.animations;
|
|
197
|
+
if (animations === undefined)
|
|
198
|
+
return;
|
|
199
|
+
if (!Array.isArray(animations)) {
|
|
200
|
+
out.push(finding(file, `"animations:" must be a list of animation config entries.`, EVIDENCE));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let usesRiskyForm = false;
|
|
204
|
+
let installedVersion;
|
|
205
|
+
let versionChecked = false;
|
|
206
|
+
animations.forEach((raw, i) => {
|
|
207
|
+
const entry = raw;
|
|
208
|
+
const label = `animations[${i}]`;
|
|
209
|
+
if (entry.entity === undefined)
|
|
210
|
+
out.push(finding(file, `${label} has no "entity:".`, EVIDENCE));
|
|
211
|
+
if (entry.set === undefined || typeof entry.set !== "string" || !normalizeSlashes(entry.set).endsWith(".anims")) {
|
|
212
|
+
out.push(finding(file, `${label}."set:" must be a .anims resource path.`, EVIDENCE));
|
|
213
|
+
}
|
|
214
|
+
if (entry.priority !== undefined && typeof entry.priority !== "number") {
|
|
215
|
+
out.push(finding(file, `${label}."priority:" must be a number when present.`, EVIDENCE));
|
|
216
|
+
}
|
|
217
|
+
const entityIsList = Array.isArray(entry.entity);
|
|
218
|
+
const hasVars = Array.isArray(entry.vars) && entry.vars.length > 0;
|
|
219
|
+
if (entityIsList || hasVars)
|
|
220
|
+
usesRiskyForm = true;
|
|
221
|
+
});
|
|
222
|
+
if (!usesRiskyForm)
|
|
223
|
+
return;
|
|
224
|
+
if (ctx.install && ctx.surface.compat) {
|
|
225
|
+
versionChecked = true;
|
|
226
|
+
try {
|
|
227
|
+
const components = await ctx.surface.compat.detectComponents(ctx.install);
|
|
228
|
+
installedVersion = components.find((c) => c.id === "archivexl")?.version;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
installedVersion = undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!versionChecked || installedVersion === undefined) {
|
|
235
|
+
out.push(finding(file, `"animations:" uses "vars:" and/or a list-form "entity:", but the installed ArchiveXL version could not be determined. Both forms are broken by a Config.cpp parser bug in ArchiveXL <= v1.27.1 and in v1.28.0-beta.1 (fixed in v1.27.3): "vars:" is silently dropped (the set mounts unconditionally on every matching weapon) and a list-form "entity:" is rejected outright. Confirm the installed ArchiveXL version, or use the "resource: patch:" ResourcePatch route instead, which is not subject to this bug.`, EVIDENCE));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const buggy = isArchiveXLAnimConfigBuggy(installedVersion);
|
|
239
|
+
if (buggy === undefined) {
|
|
240
|
+
out.push(finding(file, `"animations:" uses "vars:" and/or a list-form "entity:", but the installed ArchiveXL version "${installedVersion}" does not parse as a comparable version, so it cannot be checked against the fix in v${ARCHIVEXL_ANIM_FIX_VERSION}.`, EVIDENCE));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!buggy)
|
|
244
|
+
return;
|
|
245
|
+
animations.forEach((raw, i) => {
|
|
246
|
+
const entry = raw;
|
|
247
|
+
const label = `animations[${i}]`;
|
|
248
|
+
if (Array.isArray(entry.vars) && entry.vars.length > 0) {
|
|
249
|
+
out.push(finding(file, `${label} uses "vars:" to gate this set, but the installed ArchiveXL v${installedVersion} has the Config.cpp parser bug that tests the wrong YAML node (variablesNode.IsScalar() instead of variableNode.IsScalar()), so entry.variables stays empty and the set mounts UNCONDITIONALLY on every weapon/state sharing its animation names, not just the gated one. Fixed upstream in v${ARCHIVEXL_ANIM_FIX_VERSION}.`, ["cp2077.anims.archivexl.animconfig-vars-parser-bug", "cp2077.anims.archivexl.pr-merged-upstream"], `Upgrade ArchiveXL to >= v${ARCHIVEXL_ANIM_FIX_VERSION}, or mount this set via "resource: patch:" (an entAnimationSetupExtensionComponent carrying its own variableNames) instead, which is not subject to this bug.`));
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(entry.entity)) {
|
|
252
|
+
out.push(finding(file, `${label} uses the list form of "entity:" (${entry.entity.length} paths), but the installed ArchiveXL v${installedVersion} has the Config.cpp parser bug where the "entity:" else-if branch repeats the scalar test instead of testing IsSequence(), so this whole entry is rejected as malformed and never mounts (no log line). Fixed upstream in v${ARCHIVEXL_ANIM_FIX_VERSION}.`, ["cp2077.anims.archivexl.animconfig-entity-list-parser-bug", "cp2077.anims.archivexl.pr-merged-upstream"], `Upgrade ArchiveXL to >= v${ARCHIVEXL_ANIM_FIX_VERSION}, or split this entry into one per entity path (each with a scalar "entity:"), or use "resource: patch:" instead.`));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
export const xlManifestLintValidator = {
|
|
257
|
+
id: ID,
|
|
258
|
+
game: GAME,
|
|
259
|
+
groups: GROUPS,
|
|
260
|
+
modTypes: MOD_TYPES,
|
|
261
|
+
summary: "Lints an ArchiveXL .xl manifest: it parses as YAML, its top-level keys and their sub-shapes match the documented set, and an animations: entry avoids the ArchiveXL <= 1.27.1/1.28.0-beta.1 Config.cpp parser bugs.",
|
|
262
|
+
appliesTo(target, _project) {
|
|
263
|
+
return target.kind === "file" && extensionOf(target.path) === ".xl";
|
|
264
|
+
},
|
|
265
|
+
appliesToDescription: 'every file with a ".xl" extension (an ArchiveXL manifest)',
|
|
266
|
+
needs: { project: true, install: true },
|
|
267
|
+
cost: "edit",
|
|
268
|
+
severity: SEVERITY,
|
|
269
|
+
evidence: EVIDENCE,
|
|
270
|
+
async run(ctx) {
|
|
271
|
+
if (ctx.target.kind !== "file")
|
|
272
|
+
return [];
|
|
273
|
+
const file = ctx.target.path;
|
|
274
|
+
let text;
|
|
275
|
+
try {
|
|
276
|
+
text = await ctx.readText(file);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
return [finding(file, `Could not read this file: ${err.message}`, EVIDENCE)];
|
|
280
|
+
}
|
|
281
|
+
const cleaned = text.replace(/^\uFEFF/, "");
|
|
282
|
+
let doc;
|
|
283
|
+
try {
|
|
284
|
+
doc = parseYaml(cleaned);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
return [finding(file, `Cannot parse this ArchiveXL manifest as YAML: ${err.message}`, EVIDENCE)];
|
|
288
|
+
}
|
|
289
|
+
if (!isRecord(doc)) {
|
|
290
|
+
return [finding(file, "This ArchiveXL manifest's root must be a mapping of top-level keys.", EVIDENCE)];
|
|
291
|
+
}
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const key of Object.keys(doc)) {
|
|
294
|
+
if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
295
|
+
out.push(finding(file, `Unknown top-level key "${key}:". The documented set is localization, factories, streaming, resource, animations, player, quest. ArchiveXL advertises other keys too (submesh visibility overrides, clothing visual tags, widget spawning) that this manifest does not use — if this is one of those, ModWright's knowledge base does not yet document its shape.`, EVIDENCE));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
lintLocalization(doc, file, out);
|
|
299
|
+
lintPlayer(doc, file, out);
|
|
300
|
+
lintFactories(doc, file, out);
|
|
301
|
+
lintStreaming(doc, file, out);
|
|
302
|
+
lintResourcePatch(doc, file, out);
|
|
303
|
+
lintQuestPhases(doc, file, out);
|
|
304
|
+
await lintAnimations(doc, file, ctx, out);
|
|
305
|
+
return out;
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
export const XL_VALIDATORS = [xlManifestLintValidator];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parseXml } from "../../core/text/xml.js";
|
|
4
|
+
export const BND4_HEADER_LENGTH = 0x40;
|
|
5
|
+
export class Bnd4HeaderError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
constructor(reason, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
this.name = "Bnd4HeaderError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function ascii(bytes, at, length) {
|
|
14
|
+
let s = "";
|
|
15
|
+
for (let i = 0; i < length; i++)
|
|
16
|
+
s += String.fromCharCode(bytes[at + i]);
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
export function parseBnd4Header(bytes) {
|
|
20
|
+
if (bytes.length < 4)
|
|
21
|
+
throw new Bnd4HeaderError("too-short", `only ${bytes.length} bytes`);
|
|
22
|
+
const magic = ascii(bytes, 0, 4);
|
|
23
|
+
if (magic !== "BND4")
|
|
24
|
+
throw new Bnd4HeaderError("bad-signature", `first four bytes are ${JSON.stringify(magic)}, not "BND4"`);
|
|
25
|
+
if (bytes.length < BND4_HEADER_LENGTH)
|
|
26
|
+
throw new Bnd4HeaderError("too-short", `only ${bytes.length} bytes; a BND4 header is ${BND4_HEADER_LENGTH}`);
|
|
27
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
28
|
+
const bigEndian = bytes[9] === 1;
|
|
29
|
+
const le = !bigEndian;
|
|
30
|
+
const header = {
|
|
31
|
+
bigEndian,
|
|
32
|
+
bitBigEndian: bytes[10] !== 1,
|
|
33
|
+
fileCount: view.getInt32(12, le),
|
|
34
|
+
headerSize: Number(view.getBigInt64(16, le)),
|
|
35
|
+
version: ascii(bytes, 24, 8).replace(/\0[\s\S]*$/, ""),
|
|
36
|
+
fileHeaderSize: Number(view.getBigInt64(32, le)),
|
|
37
|
+
headersEnd: Number(view.getBigInt64(40, le)),
|
|
38
|
+
unicode: bytes[48] === 1,
|
|
39
|
+
rawFormat: bytes[49],
|
|
40
|
+
extended: bytes[50],
|
|
41
|
+
};
|
|
42
|
+
if (header.headerSize !== BND4_HEADER_LENGTH)
|
|
43
|
+
throw new Bnd4HeaderError("bad-layout", `header size field is 0x${header.headerSize.toString(16)}, not 0x40`);
|
|
44
|
+
if (![0, 1, 4, 0x80].includes(header.extended))
|
|
45
|
+
throw new Bnd4HeaderError("bad-layout", `extended byte is 0x${header.extended.toString(16)}; SoulsFormats accepts 0, 1, 4 or 0x80`);
|
|
46
|
+
if (header.fileCount < 0)
|
|
47
|
+
throw new Bnd4HeaderError("bad-layout", `file count is ${header.fileCount}`);
|
|
48
|
+
return header;
|
|
49
|
+
}
|
|
50
|
+
export async function readBnd4Header(file) {
|
|
51
|
+
const handle = await fs.open(file, "r");
|
|
52
|
+
try {
|
|
53
|
+
const buf = new Uint8Array(BND4_HEADER_LENGTH);
|
|
54
|
+
const { bytesRead } = await handle.read(buf, 0, BND4_HEADER_LENGTH, 0);
|
|
55
|
+
return parseBnd4Header(buf.subarray(0, bytesRead));
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await handle.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function isBareBinderName(name) {
|
|
62
|
+
const ext = path.extname(name).toLowerCase();
|
|
63
|
+
return ext === ".bnd" || (ext.endsWith("bnd") && ext.length > 4);
|
|
64
|
+
}
|
|
65
|
+
export const SIDECAR_RE = /^_witchy-([a-z0-9]+)\.xml$/i;
|
|
66
|
+
export function looksLikeBinderUnpackDir(name) {
|
|
67
|
+
return (/-(?:[a-z0-9_]*(?:bnd|bhd)|bnd4|tpf)(?:-dcx)?$/i.test(name) ||
|
|
68
|
+
/(?:^|-)regulation-bin(?:-dcx)?$/i.test(name) ||
|
|
69
|
+
/-dcx$/i.test(name) ||
|
|
70
|
+
/-unpacked$/i.test(name));
|
|
71
|
+
}
|
|
72
|
+
function child(el, tag) {
|
|
73
|
+
return el.children.find((c) => c.tag === tag);
|
|
74
|
+
}
|
|
75
|
+
export function parseWitchySidecar(xml) {
|
|
76
|
+
const root = parseXml(xml);
|
|
77
|
+
const text = (tag) => child(root, tag)?.text.trim();
|
|
78
|
+
const filesEl = child(root, "files");
|
|
79
|
+
const out = { tag: root.tag, files: [], hasFilesElement: filesEl !== undefined };
|
|
80
|
+
for (const key of ["game", "version", "format", "root", "compression"]) {
|
|
81
|
+
const v = text(key);
|
|
82
|
+
if (v !== undefined && v !== "")
|
|
83
|
+
out[key] = v;
|
|
84
|
+
}
|
|
85
|
+
for (const f of filesEl?.children ?? []) {
|
|
86
|
+
if (f.tag !== "file")
|
|
87
|
+
continue;
|
|
88
|
+
const p = child(f, "path");
|
|
89
|
+
if (!p)
|
|
90
|
+
continue;
|
|
91
|
+
const entry = { path: p.text };
|
|
92
|
+
const suffix = child(f, "suffix")?.text;
|
|
93
|
+
if (suffix)
|
|
94
|
+
entry.suffix = suffix;
|
|
95
|
+
const flags = child(f, "flags")?.text.trim();
|
|
96
|
+
if (flags)
|
|
97
|
+
entry.flags = flags;
|
|
98
|
+
const id = child(f, "id")?.text.trim();
|
|
99
|
+
if (id)
|
|
100
|
+
entry.id = id;
|
|
101
|
+
out.files.push(entry);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
export function sidecarEntryPath(dir, entry) {
|
|
106
|
+
const rel = entry.path.replace(/\\/g, "/");
|
|
107
|
+
const ext = path.posix.extname(rel);
|
|
108
|
+
const stem = path.posix.basename(rel, ext);
|
|
109
|
+
const parent = path.posix.dirname(rel);
|
|
110
|
+
return path.join(dir, ...(parent === "." ? [] : parent.split("/")), `${stem}${entry.suffix ?? ""}${ext}`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { readPeFileVersion } from "../../core/compat/pe.js";
|
|
4
|
+
import { pathExists, readTextIfExists } from "../../core/fsutil.js";
|
|
5
|
+
import { locateTool, TOOL_SPECS } from "../../core/toolchain/index.js";
|
|
6
|
+
import { gameDir, me3ProfilesDir } from "./loaders.js";
|
|
7
|
+
import { SIGNATURES } from "./logs.js";
|
|
8
|
+
import { readWitchyBndParamdexVersion } from "./tools.js";
|
|
9
|
+
async function peVersionOf(file) {
|
|
10
|
+
if (!(await pathExists(file)))
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
return await readPeFileVersion(file);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function latestModEngineLog(game) {
|
|
20
|
+
const dir = path.join(game, "modengine2", "logs");
|
|
21
|
+
let names;
|
|
22
|
+
try {
|
|
23
|
+
names = await fs.readdir(dir);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const logs = names.filter((n) => /^modengine_.*\.log$/i.test(n)).sort();
|
|
29
|
+
return logs.length > 0 ? path.join(dir, logs[logs.length - 1]) : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function modEngineVersionFromLog(text) {
|
|
32
|
+
const banner = SIGNATURES.find((s) => s.id === "eldenring.modengine2.initializing");
|
|
33
|
+
for (const line of text.split(/\r?\n/, 200)) {
|
|
34
|
+
const m = banner.pattern.exec(line);
|
|
35
|
+
if (m?.groups?.version)
|
|
36
|
+
return m.groups.version;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export async function detectComponentsWith(install, options = {}) {
|
|
41
|
+
const game = gameDir(install);
|
|
42
|
+
const exe = path.join(game, "eldenring.exe");
|
|
43
|
+
const exeExists = await pathExists(exe);
|
|
44
|
+
const gameVersion = install.version ?? (await peVersionOf(exe));
|
|
45
|
+
const components = [
|
|
46
|
+
{
|
|
47
|
+
id: "game",
|
|
48
|
+
label: "Elden Ring",
|
|
49
|
+
kind: "game",
|
|
50
|
+
installed: exeExists,
|
|
51
|
+
...(gameVersion !== undefined ? { version: gameVersion } : {}),
|
|
52
|
+
source: install.version ? "install record" : "Game/eldenring.exe PE file version (whether it equals the App Ver in the patch notes is unverified; the Regulation Ver is a third axis, readable only by decrypting regulation.bin)",
|
|
53
|
+
path: exe,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const launcher = path.join(game, "modengine2_launcher.exe");
|
|
57
|
+
const me2Installed = await pathExists(launcher);
|
|
58
|
+
const me2Log = me2Installed ? await latestModEngineLog(game) : undefined;
|
|
59
|
+
const me2Text = me2Log ? await readTextIfExists(me2Log) : undefined;
|
|
60
|
+
const me2Version = me2Text ? modEngineVersionFromLog(me2Text) : undefined;
|
|
61
|
+
components.push({
|
|
62
|
+
id: "modengine2",
|
|
63
|
+
label: "Mod Engine 2",
|
|
64
|
+
kind: "loader",
|
|
65
|
+
installed: me2Installed,
|
|
66
|
+
...(me2Version !== undefined ? { version: me2Version } : {}),
|
|
67
|
+
source: me2Version ? `${me2Log}: ModEngine version banner` : me2Installed ? "modengine2_launcher.exe present; no log banner read (not launched, or the log rotated out)" : "modengine2_launcher.exe not found beside the executable",
|
|
68
|
+
path: launcher,
|
|
69
|
+
});
|
|
70
|
+
const profilesDir = me3ProfilesDir();
|
|
71
|
+
let me3Exe;
|
|
72
|
+
let witchyExe;
|
|
73
|
+
if (options.locateTools !== false) {
|
|
74
|
+
const me3Spec = TOOL_SPECS.find((t) => t.id === "me3");
|
|
75
|
+
const witchySpec = TOOL_SPECS.find((t) => t.id === "witchybnd");
|
|
76
|
+
const [me3, witchy] = await Promise.all([me3Spec ? locateTool(me3Spec) : undefined, witchySpec ? locateTool(witchySpec) : undefined]);
|
|
77
|
+
if (me3?.found && me3.path)
|
|
78
|
+
me3Exe = me3.path;
|
|
79
|
+
if (witchy?.found && witchy.path)
|
|
80
|
+
witchyExe = witchy.path;
|
|
81
|
+
const me3Version = me3?.found && me3.version && me3.version !== "unknown" ? me3.version : undefined;
|
|
82
|
+
const profilesExist = await pathExists(profilesDir);
|
|
83
|
+
components.push({
|
|
84
|
+
id: "me3",
|
|
85
|
+
label: "me3",
|
|
86
|
+
kind: "loader",
|
|
87
|
+
installed: me3Exe !== undefined || profilesExist,
|
|
88
|
+
...(me3Version !== undefined ? { version: me3Version } : {}),
|
|
89
|
+
source: me3Exe ? `${me3Exe} (me3 --version)` : profilesExist ? `${profilesDir} exists; the me3 executable was not located` : "no me3 executable located and no profiles directory",
|
|
90
|
+
...(me3Exe ? { path: me3Exe } : {}),
|
|
91
|
+
});
|
|
92
|
+
const paramdexVersion = witchyExe ? await readWitchyBndParamdexVersion(witchyExe) : undefined;
|
|
93
|
+
components.push({
|
|
94
|
+
id: "witchybnd",
|
|
95
|
+
label: "WitchyBND",
|
|
96
|
+
kind: "tool",
|
|
97
|
+
installed: witchyExe !== undefined,
|
|
98
|
+
...(witchy?.found && witchy.version && witchy.version !== "unknown" ? { version: witchy.version } : {}),
|
|
99
|
+
source: witchyExe ? `${witchyExe} (WitchyBND -v)${paramdexVersion ? `; its bundled Paramdex repacks regulations up to ${paramdexVersion} (Assets/Paramdex/ER/Upgrader/version.txt)` : ""}` : "not located",
|
|
100
|
+
...(witchyExe ? { path: witchyExe } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
components.push({ id: "me3", label: "me3", kind: "loader", installed: await pathExists(profilesDir), source: "profiles directory only (tool location skipped)" });
|
|
105
|
+
components.push({ id: "witchybnd", label: "WitchyBND", kind: "tool", installed: false, source: "tool location skipped" });
|
|
106
|
+
}
|
|
107
|
+
const ersc = path.join(game, "SeamlessCoop", "ersc.dll");
|
|
108
|
+
const erscInstalled = await pathExists(ersc);
|
|
109
|
+
components.push({
|
|
110
|
+
id: "seamless-coop",
|
|
111
|
+
label: "Seamless Co-op",
|
|
112
|
+
kind: "mod",
|
|
113
|
+
installed: erscInstalled,
|
|
114
|
+
source: erscInstalled ? "Game/SeamlessCoop/ersc.dll present; its version is not on disk in a form read here (releases are titled by game version)" : "Game/SeamlessCoop/ersc.dll not found",
|
|
115
|
+
path: ersc,
|
|
116
|
+
});
|
|
117
|
+
return components;
|
|
118
|
+
}
|
|
119
|
+
export const RULES = [
|
|
120
|
+
{
|
|
121
|
+
id: "er-modengine2-archived",
|
|
122
|
+
game: "eldenring",
|
|
123
|
+
component: "modengine2",
|
|
124
|
+
requires: [],
|
|
125
|
+
advisory: true,
|
|
126
|
+
severity: "warn",
|
|
127
|
+
note: "Mod Engine 2 is discontinued: its last release (2.1.0, 2023-09-01) and the repository's archiving (2024-07-19) both predate the Shadow of the Erdtree DLC, so on any game newer than that it is untested by its authors; me3 is the maintained loader.",
|
|
128
|
+
source: "https://github.com/soulsmods/ModEngine2 (archive banner, releases)",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "er-modengine2-with-game",
|
|
132
|
+
game: "eldenring",
|
|
133
|
+
component: "modengine2",
|
|
134
|
+
requires: [{ component: "game" }],
|
|
135
|
+
advisory: true,
|
|
136
|
+
severity: "info",
|
|
137
|
+
versionGate: "game",
|
|
138
|
+
undeterminedSeverity: "info",
|
|
139
|
+
note: "The game's version here is its PE file version; whether it equals the App Ver the patch notes use (1.12 introduced the DLC) is unverified, so no floor is compared. If this game is 1.12 or newer, Mod Engine 2's last release never saw it.",
|
|
140
|
+
source: "ModWright research",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "er-seamless-coop-tracks-game",
|
|
144
|
+
game: "eldenring",
|
|
145
|
+
component: "seamless-coop",
|
|
146
|
+
requires: [{ component: "game" }],
|
|
147
|
+
advisory: true,
|
|
148
|
+
severity: "info",
|
|
149
|
+
note: "Seamless Co-op's releases are titled by the game version they update to (v2.0.1: 'Update to game ver 1.17'); after a game patch, check its releases page before launching, and note it must run through ersc_launcher.exe or Mod Engine 2, not a DLL injector.",
|
|
150
|
+
source: "https://github.com/LukeYui/EldenRingSeamlessCoopRelease releases",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "er-witchybnd-paramdex-axis",
|
|
154
|
+
game: "eldenring",
|
|
155
|
+
component: "witchybnd",
|
|
156
|
+
requires: [],
|
|
157
|
+
advisory: true,
|
|
158
|
+
severity: "info",
|
|
159
|
+
note: "WitchyBND refuses to repack a regulation newer than the newest version its bundled Paramdex knows (Assets/Paramdex/ER/Upgrader/version.txt, reported in this component's source); the game's Regulation Ver is a separate axis that only DSMSPortable (-P gamepath) or a decrypt can read, so compare the two by hand after a game patch.",
|
|
160
|
+
source: "WitchyBND changelog v2.1.1.0",
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
export const eldenringCompat = {
|
|
164
|
+
detectComponents: (install) => detectComponentsWith(install),
|
|
165
|
+
rules: RULES,
|
|
166
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
export const DCX_HEADER_LENGTH = 0x34;
|
|
3
|
+
export const DCX_CODECS = ["DFLT", "EDGE", "KRAK", "ZSTD"];
|
|
4
|
+
export class DcxHeaderError extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.name = "DcxHeaderError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function ascii(bytes, at) {
|
|
13
|
+
return String.fromCharCode(bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]);
|
|
14
|
+
}
|
|
15
|
+
export function parseDcxHeader(bytes) {
|
|
16
|
+
if (bytes.length < 8)
|
|
17
|
+
throw new DcxHeaderError("too-short", `only ${bytes.length} bytes; a DCX header is ${DCX_HEADER_LENGTH}`);
|
|
18
|
+
const magic = ascii(bytes, 0);
|
|
19
|
+
if (magic === "DCP\0")
|
|
20
|
+
return { magic: "DCP", codec: ascii(bytes, 4) };
|
|
21
|
+
if (magic !== "DCX\0")
|
|
22
|
+
throw new DcxHeaderError("bad-signature", `first four bytes are ${JSON.stringify(magic)}, not "DCX\\0"`);
|
|
23
|
+
if (bytes.length < DCX_HEADER_LENGTH)
|
|
24
|
+
throw new DcxHeaderError("too-short", `only ${bytes.length} bytes; a DCX header is ${DCX_HEADER_LENGTH}`);
|
|
25
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
26
|
+
if (ascii(bytes, 0x18) !== "DCS\0")
|
|
27
|
+
throw new DcxHeaderError("bad-layout", `no "DCS\\0" at 0x18`);
|
|
28
|
+
if (ascii(bytes, 0x24) !== "DCP\0")
|
|
29
|
+
throw new DcxHeaderError("bad-layout", `no "DCP\\0" at 0x24`);
|
|
30
|
+
return {
|
|
31
|
+
magic: "DCX",
|
|
32
|
+
variant: view.getUint32(0x04, false),
|
|
33
|
+
uncompressedSize: view.getUint32(0x1c, false),
|
|
34
|
+
compressedSize: view.getUint32(0x20, false),
|
|
35
|
+
codec: ascii(bytes, 0x28),
|
|
36
|
+
level: bytes[0x30],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function readDcxHeader(file) {
|
|
40
|
+
const handle = await fs.open(file, "r");
|
|
41
|
+
try {
|
|
42
|
+
const buf = new Uint8Array(DCX_HEADER_LENGTH);
|
|
43
|
+
const { bytesRead } = await handle.read(buf, 0, DCX_HEADER_LENGTH, 0);
|
|
44
|
+
return parseDcxHeader(buf.subarray(0, bytesRead));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await handle.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { eldenring } from "./surface.js";
|
|
2
|
+
export { SIGNATURES } from "./logs.js";
|
|
3
|
+
export { parseMe3Profile, parseMe3ProfileDetailed, parseModEngine2Config, readModEngine2LoadOrder, MODENGINE2_CONFIG, ME3_GAME_IDS, ME3_ELDENRING_IDS, ME3_PROFILE_VERSION, me3ProfilesDir } from "./loaders.js";
|
|
4
|
+
export { parseParamdef, parseParamdefFieldDef, parseTdf, loadParamdex, findParamdef, checkFieldValue, ParamdefError, PARAMDEF_TYPES } from "./paramdef.js";
|
|
5
|
+
export { parseDsmsCsv, parseWitchyParamXml, detectCsvDelimiter } from "./params.js";
|
|
6
|
+
export { parseDcxHeader, readDcxHeader, DcxHeaderError, DCX_CODECS } from "./dcx.js";
|
|
7
|
+
export { DSMSPORTABLE_EXIT_CODES, explainDsmsPortableExit, readWitchyBndParamdexVersion, witchyBndParamdexDir } from "./tools.js";
|
|
8
|
+
export { PARAM_VALIDATORS, resolveParamdexDir, loadParamdexCached, clearParamdexCache, PARAMDEX_ENV } from "./validators/params.js";
|
|
9
|
+
export { PROFILE_VALIDATORS } from "./validators/profiles.js";
|
|
10
|
+
export { PACKAGE_VALIDATORS, PACKAGE_DIRECTORY_VOCABULARY } from "./validators/packages.js";
|
|
11
|
+
export { BINDER_VALIDATORS } from "./validators/binders.js";
|
|
12
|
+
export { parseBnd4Header, readBnd4Header, Bnd4HeaderError, parseWitchySidecar, sidecarEntryPath, looksLikeBinderUnpackDir, isBareBinderName } from "./binders.js";
|
|
13
|
+
export { eldenringCompat, detectComponentsWith, modEngineVersionFromLog } from "./compat.js";
|