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.
Files changed (455) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +212 -0
  3. package/assets/claude/agents/mw-explore.md +46 -0
  4. package/assets/claude/agents/mw-fixture.md +33 -0
  5. package/assets/claude/agents/mw-rule-author.md +51 -0
  6. package/assets/claude/agents/mw-scaffold-author.md +48 -0
  7. package/assets/claude/skills/mw-author.md +59 -0
  8. package/assets/claude/skills/mw-new-item.md +51 -0
  9. package/assets/claude/skills/mw-new-spell.md +50 -0
  10. package/assets/claude/skills/mw-scaffold.md +59 -0
  11. package/assets/claude/skills/mw-ship.md +57 -0
  12. package/assets/claude/skills/mw-validate.md +51 -0
  13. package/assets/claude/skills/mw-verify.md +59 -0
  14. package/assets/nivalisnights/extract_resources.py +177 -0
  15. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
  16. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
  17. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
  18. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
  19. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
  20. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
  21. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
  22. package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
  23. package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
  24. package/bridges/bg3-se/README.md +99 -0
  25. package/bridges/bg3-se/bridge.json +68 -0
  26. package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
  27. package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
  28. package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
  29. package/bridges/cp2077-cet/README.md +165 -0
  30. package/bridges/cp2077-cet/bridge.json +53 -0
  31. package/dist/core/bridge/arm.js +175 -0
  32. package/dist/core/bridge/cet-console.js +26 -0
  33. package/dist/core/bridge/cet-log.js +362 -0
  34. package/dist/core/bridge/index.js +11 -0
  35. package/dist/core/bridge/locate.js +35 -0
  36. package/dist/core/bridge/paths.js +34 -0
  37. package/dist/core/bridge/pending.js +89 -0
  38. package/dist/core/bridge/poll.js +155 -0
  39. package/dist/core/bridge/stage.js +100 -0
  40. package/dist/core/bridge/status.js +97 -0
  41. package/dist/core/bridge/types.js +145 -0
  42. package/dist/core/bridge/validate.js +107 -0
  43. package/dist/core/build/index.js +6 -0
  44. package/dist/core/build/manifest.js +47 -0
  45. package/dist/core/build/paths.js +32 -0
  46. package/dist/core/build/run.js +342 -0
  47. package/dist/core/build/stage.js +265 -0
  48. package/dist/core/build/steps/bg3.js +129 -0
  49. package/dist/core/build/steps/core.js +195 -0
  50. package/dist/core/build/steps/cyberpunk.js +95 -0
  51. package/dist/core/build/steps/listing.js +35 -0
  52. package/dist/core/build/types.js +13 -0
  53. package/dist/core/build/zip.js +148 -0
  54. package/dist/core/compat/check.js +113 -0
  55. package/dist/core/compat/pe.js +220 -0
  56. package/dist/core/compat/types.js +1 -0
  57. package/dist/core/compat/version.js +70 -0
  58. package/dist/core/deploy/guards.js +292 -0
  59. package/dist/core/deploy/index.js +5 -0
  60. package/dist/core/deploy/plan.js +696 -0
  61. package/dist/core/deploy/reload-apply.js +361 -0
  62. package/dist/core/deploy/reload.js +358 -0
  63. package/dist/core/deploy/sources.js +169 -0
  64. package/dist/core/deploy/types.js +1 -0
  65. package/dist/core/epic.js +54 -0
  66. package/dist/core/fsutil.js +191 -0
  67. package/dist/core/home.js +70 -0
  68. package/dist/core/knowledge/facts.js +229 -0
  69. package/dist/core/knowledge/index.js +4 -0
  70. package/dist/core/knowledge/search.js +112 -0
  71. package/dist/core/knowledge/store.js +123 -0
  72. package/dist/core/knowledge/types.js +1 -0
  73. package/dist/core/ledger/check.js +42 -0
  74. package/dist/core/ledger/hooks.js +80 -0
  75. package/dist/core/ledger/index.js +7 -0
  76. package/dist/core/ledger/query.js +17 -0
  77. package/dist/core/ledger/record.js +84 -0
  78. package/dist/core/ledger/status.js +53 -0
  79. package/dist/core/ledger/store.js +98 -0
  80. package/dist/core/ledger/types.js +108 -0
  81. package/dist/core/logs/index.js +4 -0
  82. package/dist/core/logs/registry.js +50 -0
  83. package/dist/core/logs/scan.js +195 -0
  84. package/dist/core/logs/triage.js +160 -0
  85. package/dist/core/logs/types.js +1 -0
  86. package/dist/core/lsx.js +10 -0
  87. package/dist/core/lz4.js +66 -0
  88. package/dist/core/mods.js +229 -0
  89. package/dist/core/process.js +49 -0
  90. package/dist/core/project/build-steps.js +83 -0
  91. package/dist/core/project/describe.js +74 -0
  92. package/dist/core/project/index.js +5 -0
  93. package/dist/core/project/load.js +146 -0
  94. package/dist/core/project/schema.js +264 -0
  95. package/dist/core/project/types.js +3 -0
  96. package/dist/core/prompts.js +72 -0
  97. package/dist/core/registry.js +35 -0
  98. package/dist/core/safety/backup.js +206 -0
  99. package/dist/core/safety/execute.js +188 -0
  100. package/dist/core/safety/index.js +5 -0
  101. package/dist/core/safety/plan.js +122 -0
  102. package/dist/core/safety/restore.js +172 -0
  103. package/dist/core/safety/types.js +1 -0
  104. package/dist/core/scaffold/bootstrap.js +7 -0
  105. package/dist/core/scaffold/catalog.js +28 -0
  106. package/dist/core/scaffold/claude-assets.js +271 -0
  107. package/dist/core/scaffold/collisions.js +64 -0
  108. package/dist/core/scaffold/emit.js +356 -0
  109. package/dist/core/scaffold/engine.js +680 -0
  110. package/dist/core/scaffold/ids.js +78 -0
  111. package/dist/core/scaffold/index.js +10 -0
  112. package/dist/core/scaffold/jsonpath.js +121 -0
  113. package/dist/core/scaffold/load.js +81 -0
  114. package/dist/core/scaffold/render.js +85 -0
  115. package/dist/core/scaffold/sequence.js +396 -0
  116. package/dist/core/scaffold/types.js +309 -0
  117. package/dist/core/steam.js +63 -0
  118. package/dist/core/surfaceutil.js +36 -0
  119. package/dist/core/testplan/check.js +67 -0
  120. package/dist/core/testplan/index.js +5 -0
  121. package/dist/core/testplan/load.js +100 -0
  122. package/dist/core/testplan/registry.js +317 -0
  123. package/dist/core/testplan/run.js +389 -0
  124. package/dist/core/testplan/types.js +82 -0
  125. package/dist/core/text/converters/bg3.js +50 -0
  126. package/dist/core/text/converters/cyberpunk.js +36 -0
  127. package/dist/core/text/converters/index.js +261 -0
  128. package/dist/core/text/diff/cr2w.js +180 -0
  129. package/dist/core/text/diff/index.js +16 -0
  130. package/dist/core/text/diff/loca.js +43 -0
  131. package/dist/core/text/diff/lsx.js +126 -0
  132. package/dist/core/text/handles.js +86 -0
  133. package/dist/core/text/index.js +6 -0
  134. package/dist/core/text/json.js +46 -0
  135. package/dist/core/text/templates/cache.js +71 -0
  136. package/dist/core/text/templates/index.js +3 -0
  137. package/dist/core/text/templates/library.js +245 -0
  138. package/dist/core/text/templates/types.js +51 -0
  139. package/dist/core/text/toolchain.js +16 -0
  140. package/dist/core/text/types.js +1 -0
  141. package/dist/core/text/validate.js +149 -0
  142. package/dist/core/text/xml.js +58 -0
  143. package/dist/core/toolchain/datafetch.js +151 -0
  144. package/dist/core/toolchain/env.js +63 -0
  145. package/dist/core/toolchain/features.js +170 -0
  146. package/dist/core/toolchain/index.js +8 -0
  147. package/dist/core/toolchain/install.js +191 -0
  148. package/dist/core/toolchain/locate.js +275 -0
  149. package/dist/core/toolchain/remember.js +84 -0
  150. package/dist/core/toolchain/run.js +318 -0
  151. package/dist/core/toolchain/specs.js +351 -0
  152. package/dist/core/toolchain/types.js +1 -0
  153. package/dist/core/types.js +1 -0
  154. package/dist/core/userconfig.js +66 -0
  155. package/dist/core/validate/context.js +139 -0
  156. package/dist/core/validate/index.js +5 -0
  157. package/dist/core/validate/registry.js +58 -0
  158. package/dist/core/validate/run.js +131 -0
  159. package/dist/core/validate/suppressions.js +91 -0
  160. package/dist/core/validate/types.js +2 -0
  161. package/dist/core/yamledit.js +51 -0
  162. package/dist/index.js +389 -0
  163. package/dist/server.js +1914 -0
  164. package/dist/surfaces/baldursgate3/compat.js +164 -0
  165. package/dist/surfaces/baldursgate3/identity.js +110 -0
  166. package/dist/surfaces/baldursgate3/index/build.js +331 -0
  167. package/dist/surfaces/baldursgate3/index/index.js +17 -0
  168. package/dist/surfaces/baldursgate3/index/info.js +40 -0
  169. package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
  170. package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
  171. package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
  172. package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
  173. package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
  174. package/dist/surfaces/baldursgate3/index/queries.js +327 -0
  175. package/dist/surfaces/baldursgate3/index/schema.js +62 -0
  176. package/dist/surfaces/baldursgate3/index.js +3 -0
  177. package/dist/surfaces/baldursgate3/logs.js +278 -0
  178. package/dist/surfaces/baldursgate3/modsettings.js +115 -0
  179. package/dist/surfaces/baldursgate3/pak.js +153 -0
  180. package/dist/surfaces/baldursgate3/settings.js +49 -0
  181. package/dist/surfaces/baldursgate3/surface.js +232 -0
  182. package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
  183. package/dist/surfaces/baldursgate3/validators/files.js +476 -0
  184. package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
  185. package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
  186. package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
  187. package/dist/surfaces/baldursgate3/validators/story.js +790 -0
  188. package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
  189. package/dist/surfaces/cyberpunk2077/compat.js +223 -0
  190. package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
  191. package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
  192. package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
  193. package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
  194. package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
  195. package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
  196. package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
  197. package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
  198. package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
  199. package/dist/surfaces/cyberpunk2077/index.js +3 -0
  200. package/dist/surfaces/cyberpunk2077/logs.js +742 -0
  201. package/dist/surfaces/cyberpunk2077/paths.js +12 -0
  202. package/dist/surfaces/cyberpunk2077/surface.js +218 -0
  203. package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
  204. package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
  205. package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
  206. package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
  207. package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
  208. package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
  209. package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
  210. package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
  211. package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
  212. package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
  213. package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
  214. package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
  215. package/dist/surfaces/eldenring/binders.js +111 -0
  216. package/dist/surfaces/eldenring/compat.js +166 -0
  217. package/dist/surfaces/eldenring/dcx.js +49 -0
  218. package/dist/surfaces/eldenring/index.js +13 -0
  219. package/dist/surfaces/eldenring/loaders.js +132 -0
  220. package/dist/surfaces/eldenring/logs.js +67 -0
  221. package/dist/surfaces/eldenring/paramdef.js +256 -0
  222. package/dist/surfaces/eldenring/params.js +129 -0
  223. package/dist/surfaces/eldenring/surface.js +121 -0
  224. package/dist/surfaces/eldenring/tools.js +52 -0
  225. package/dist/surfaces/eldenring/validators/binders.js +193 -0
  226. package/dist/surfaces/eldenring/validators/packages.js +159 -0
  227. package/dist/surfaces/eldenring/validators/params.js +334 -0
  228. package/dist/surfaces/eldenring/validators/profiles.js +287 -0
  229. package/dist/surfaces/index.js +34 -0
  230. package/dist/surfaces/nivalisnights/compat.js +229 -0
  231. package/dist/surfaces/nivalisnights/index/build.js +310 -0
  232. package/dist/surfaces/nivalisnights/index/index.js +5 -0
  233. package/dist/surfaces/nivalisnights/index/queries.js +231 -0
  234. package/dist/surfaces/nivalisnights/index.js +4 -0
  235. package/dist/surfaces/nivalisnights/logs.js +473 -0
  236. package/dist/surfaces/nivalisnights/surface.js +195 -0
  237. package/dist/surfaces/skyrimse/archives.js +229 -0
  238. package/dist/surfaces/skyrimse/compat.js +163 -0
  239. package/dist/surfaces/skyrimse/index.js +12 -0
  240. package/dist/surfaces/skyrimse/loadorder.js +174 -0
  241. package/dist/surfaces/skyrimse/logs.js +241 -0
  242. package/dist/surfaces/skyrimse/plugins.js +149 -0
  243. package/dist/surfaces/skyrimse/records.js +94 -0
  244. package/dist/surfaces/skyrimse/surface.js +240 -0
  245. package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
  246. package/dist/surfaces/skyrimse/validators/archives.js +110 -0
  247. package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
  248. package/dist/surfaces/skyrimse/validators/records.js +117 -0
  249. package/dist/surfaces/stardewvalley/compat.js +190 -0
  250. package/dist/surfaces/stardewvalley/config.js +42 -0
  251. package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
  252. package/dist/surfaces/stardewvalley/index/index.js +229 -0
  253. package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
  254. package/dist/surfaces/stardewvalley/index.js +12 -0
  255. package/dist/surfaces/stardewvalley/logs.js +307 -0
  256. package/dist/surfaces/stardewvalley/mods.js +182 -0
  257. package/dist/surfaces/stardewvalley/surface.js +244 -0
  258. package/dist/surfaces/stardewvalley/validators/content.js +321 -0
  259. package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
  260. package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
  261. package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
  262. package/dist/surfaces/subnautica2/compat.js +138 -0
  263. package/dist/surfaces/subnautica2/index.js +5 -0
  264. package/dist/surfaces/subnautica2/logs.js +129 -0
  265. package/dist/surfaces/subnautica2/paks.js +173 -0
  266. package/dist/surfaces/subnautica2/surface.js +151 -0
  267. package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
  268. package/dist/surfaces/valheim/compat.js +171 -0
  269. package/dist/surfaces/valheim/index/index.js +301 -0
  270. package/dist/surfaces/valheim/index/tables.js +103 -0
  271. package/dist/surfaces/valheim/index.js +11 -0
  272. package/dist/surfaces/valheim/logs.js +197 -0
  273. package/dist/surfaces/valheim/packaging.js +131 -0
  274. package/dist/surfaces/valheim/plugins.js +270 -0
  275. package/dist/surfaces/valheim/references.js +121 -0
  276. package/dist/surfaces/valheim/surface.js +223 -0
  277. package/dist/surfaces/valheim/validators/packaging.js +218 -0
  278. package/dist/surfaces/valheim/validators/plugins.js +287 -0
  279. package/dist/surfaces/valheim/validators/references.js +169 -0
  280. package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
  281. package/knowledge/baldursgate3/audio.voice.yaml +182 -0
  282. package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
  283. package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
  284. package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
  285. package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
  286. package/knowledge/baldursgate3/crash.causes.yaml +90 -0
  287. package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
  288. package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
  289. package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
  290. package/knowledge/baldursgate3/dialogue.yaml +122 -0
  291. package/knowledge/baldursgate3/feats.classes.yaml +164 -0
  292. package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
  293. package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
  294. package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
  295. package/knowledge/baldursgate3/journal.quests.yaml +214 -0
  296. package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
  297. package/knowledge/baldursgate3/localization.handles.yaml +92 -0
  298. package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
  299. package/knowledge/baldursgate3/origins.companions.yaml +144 -0
  300. package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
  301. package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
  302. package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
  303. package/knowledge/baldursgate3/process.launch.yaml +34 -0
  304. package/knowledge/baldursgate3/progressions.yaml +92 -0
  305. package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
  306. package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
  307. package/knowledge/baldursgate3/races.progression.yaml +228 -0
  308. package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
  309. package/knowledge/baldursgate3/scriptextender.yaml +252 -0
  310. package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
  311. package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
  312. package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
  313. package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
  314. package/knowledge/baldursgate3/stats.spells.yaml +293 -0
  315. package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
  316. package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
  317. package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
  318. package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
  319. package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
  320. package/knowledge/baldursgate3/ui.icons.yaml +412 -0
  321. package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
  322. package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
  323. package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
  324. package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
  325. package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
  326. package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
  327. package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
  328. package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
  329. package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
  330. package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
  331. package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
  332. package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
  333. package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
  334. package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
  335. package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
  336. package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
  337. package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
  338. package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
  339. package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
  340. package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
  341. package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
  342. package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
  343. package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
  344. package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
  345. package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
  346. package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
  347. package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
  348. package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
  349. package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
  350. package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
  351. package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
  352. package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
  353. package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
  354. package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
  355. package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
  356. package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
  357. package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
  358. package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
  359. package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
  360. package/knowledge/cyberpunk2077/player.control.yaml +918 -0
  361. package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
  362. package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
  363. package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
  364. package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
  365. package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
  366. package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
  367. package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
  368. package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
  369. package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
  370. package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
  371. package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
  372. package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
  373. package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
  374. package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
  375. package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
  376. package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
  377. package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
  378. package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
  379. package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
  380. package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
  381. package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
  382. package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
  383. package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
  384. package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
  385. package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
  386. package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
  387. package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
  388. package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
  389. package/knowledge/eldenring/formats.binders.yaml +237 -0
  390. package/knowledge/eldenring/launcher.me3.yaml +230 -0
  391. package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
  392. package/knowledge/eldenring/params.paramdef.yaml +149 -0
  393. package/knowledge/eldenring/params.regulation.yaml +142 -0
  394. package/knowledge/eldenring/saves.online.yaml +118 -0
  395. package/knowledge/eldenring/toolchain.cli.yaml +256 -0
  396. package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
  397. package/knowledge/skyrimse/archives.bsa.yaml +212 -0
  398. package/knowledge/skyrimse/install.detection.yaml +41 -0
  399. package/knowledge/skyrimse/loadorder.files.yaml +242 -0
  400. package/knowledge/skyrimse/plugins.esl.yaml +189 -0
  401. package/knowledge/skyrimse/plugins.format.yaml +224 -0
  402. package/knowledge/skyrimse/skse.runtime.yaml +283 -0
  403. package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
  404. package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
  405. package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
  406. package/knowledge/stardewvalley/game.versions.yaml +202 -0
  407. package/knowledge/stardewvalley/install.layout.yaml +277 -0
  408. package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
  409. package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
  410. package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
  411. package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
  412. package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
  413. package/knowledge/subnautica2/game.build.yaml +331 -0
  414. package/knowledge/subnautica2/modding.state.yaml +210 -0
  415. package/knowledge/subnautica2/ue5.containers.yaml +72 -0
  416. package/knowledge/subnautica2/ue5.paks.yaml +203 -0
  417. package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
  418. package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
  419. package/knowledge/valheim/bepinex.loader.yaml +247 -0
  420. package/knowledge/valheim/bepinex.plugins.yaml +251 -0
  421. package/knowledge/valheim/game.versions.yaml +321 -0
  422. package/knowledge/valheim/install.layout.yaml +216 -0
  423. package/knowledge/valheim/jotunn.library.yaml +334 -0
  424. package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
  425. package/knowledge/valheim/toolchain.build.yaml +111 -0
  426. package/package.json +29 -0
  427. package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
  428. package/scaffolds/baldursgate3/new-item.yaml +264 -0
  429. package/scaffolds/baldursgate3/new-mod.yaml +174 -0
  430. package/scaffolds/baldursgate3/new-passive.yaml +100 -0
  431. package/scaffolds/baldursgate3/new-project.yaml +118 -0
  432. package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
  433. package/scaffolds/baldursgate3/new-spell.yaml +102 -0
  434. package/scaffolds/baldursgate3/new-status.yaml +119 -0
  435. package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
  436. package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
  437. package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
  438. package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
  439. package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
  440. package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
  441. package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
  442. package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
  443. package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
  444. package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
  445. package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
  446. package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
  447. package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
  448. package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
  449. package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
  450. package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
  451. package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
  452. package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
  453. package/scaffolds/stardewvalley/new-project.yaml +184 -0
  454. package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
  455. package/scripts/mw-tool.mjs +184 -0
@@ -0,0 +1,287 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { compareVersions, parseVersion } from "../../../core/compat/version.js";
4
+ import { parseDependencyString, parseThunderstoreManifest } from "../packaging.js";
5
+ import { BEPINPLUGIN_GUID_RE, COMPATIBILITY_LEVELS, packageNameMatchesGuid, scanCfg, scanPluginSource, SEMVER_RE, SYSTEM_VERSION_RE, VERSION_STRICTNESS } from "../plugins.js";
6
+ import { PACKAGE_SOURCE_KIND } from "./packaging.js";
7
+ export const SOURCE_SOURCE_KIND = "source";
8
+ const PLUGIN_EVIDENCE = ["valheim.bepinex.plugins.bepinplugin-attribute-contract", "valheim.bepinex.plugins.valheim-pack-allows-semver-versions"];
9
+ const DEPENDENCY_EVIDENCE = ["valheim.bepinex.plugins.dependency-attributes", "valheim.bepinex.plugins.missing-hard-dependency-lines", "valheim.thunderstore.packaging.dependency-string-format", "valheim.toolchain.build.guid-to-package-name-has-no-registry"];
10
+ const HARMONY_EVIDENCE = ["valheim.bepinex.plugins.never-unpatchall-with-null", "valheim.bepinex.plugins.harmony-version-and-patch-shape"];
11
+ const NETWORK_EVIDENCE = ["valheim.jotunn.library.networkcompatibility-attribute"];
12
+ const CFG_EVIDENCE = ["valheim.bepinex.loader.bepinex-cfg-keys"];
13
+ function isWithin(parent, child) {
14
+ const rel = path.relative(parent, child);
15
+ return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
16
+ }
17
+ function isCsUnderSource(target, project) {
18
+ if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ".cs")
19
+ return false;
20
+ return project.sources.some((s) => s.exists && s.kind === SOURCE_SOURCE_KIND && isWithin(s.absPath, target.path));
21
+ }
22
+ const CS_APPLIES = `every .cs file under a project source of kind "${SOURCE_SOURCE_KIND}"`;
23
+ async function scan(ctx, file) {
24
+ return scanPluginSource(await ctx.readText(file));
25
+ }
26
+ const bepInPlugin = {
27
+ id: "valheim.plugins.bepinplugin",
28
+ game: "valheim",
29
+ groups: ["plugins"],
30
+ modTypes: ["gameplay-tweak", "packaging"],
31
+ summary: "A class deriving from BaseUnityPlugin carries exactly one [BepInPlugin]; its GUID matches BepInEx's allowed pattern and its version parses (System.Version, or SemVer under the Valheim pack); a [BepInPlugin] on a class that is not a BaseUnityPlugin is ignored by the chainloader.",
32
+ appliesTo: isCsUnderSource,
33
+ appliesToDescription: CS_APPLIES,
34
+ needs: { project: true },
35
+ cost: "edit",
36
+ severity: "block",
37
+ evidence: PLUGIN_EVIDENCE,
38
+ async run(ctx) {
39
+ if (ctx.target.kind !== "file")
40
+ return [];
41
+ const file = ctx.target.path;
42
+ const name = path.basename(file);
43
+ const s = await scan(ctx, file);
44
+ const findings = [];
45
+ if (s.baseUnityPluginClasses.length > 0 && s.plugins.length === 0) {
46
+ for (const c of s.baseUnityPluginClasses) {
47
+ findings.push({ validatorId: this.id, severity: "block", file, line: c.line, message: `${c.name} in ${name} derives from BaseUnityPlugin but carries no [BepInPlugin(GUID, Name, Version)]; BepInEx ignores the type entirely.`, fix: "Add [BepInPlugin(\"com.example.mod\", \"Mod Name\", \"1.0.0\")] above the class.", evidence: PLUGIN_EVIDENCE });
48
+ }
49
+ }
50
+ if (s.plugins.length > 1 && s.baseUnityPluginClasses.length <= 1) {
51
+ findings.push({ validatorId: this.id, severity: "block", file, line: s.plugins[1].line, message: `${name} carries ${s.plugins.length} [BepInPlugin] attributes for ${s.baseUnityPluginClasses.length} BaseUnityPlugin class${s.baseUnityPluginClasses.length === 1 ? "" : "es"}; each plugin class carries exactly one.`, evidence: PLUGIN_EVIDENCE });
52
+ }
53
+ if (s.plugins.length > 0 && s.baseUnityPluginClasses.length === 0) {
54
+ findings.push({ validatorId: this.id, severity: "warn", file, line: s.plugins[0].line, message: `${name} has a [BepInPlugin] but no class in this file derives from BaseUnityPlugin; the chainloader only loads BaseUnityPlugin subclasses, so if the class is declared here it will be skipped.`, evidence: PLUGIN_EVIDENCE });
55
+ }
56
+ for (const p of s.plugins) {
57
+ if (p.unresolved.length > 0) {
58
+ findings.push({ validatorId: this.id, severity: "info", file, line: p.line, message: `[BepInPlugin] in ${name} has argument${p.unresolved.length === 1 ? "" : "s"} ${p.unresolved.map((u) => `'${u}'`).join(", ")} not resolvable from this file (not a string literal or a same-file const string), so the GUID and version were not checked.`, evidence: PLUGIN_EVIDENCE });
59
+ }
60
+ if (p.guid !== undefined && !BEPINPLUGIN_GUID_RE.test(p.guid)) {
61
+ findings.push({ validatorId: this.id, severity: "block", file, line: p.line, message: `GUID "${p.guid}" in ${name} is not of BepInEx's allowed form (letters, digits, '.', '_', '-'); the chainloader logs "Skipping type ... because its GUID ... is of an illegal format" and never loads it.`, fix: "Use reverse-domain notation, e.g. com.author.modname.", evidence: PLUGIN_EVIDENCE });
62
+ }
63
+ if (p.version !== undefined && !SYSTEM_VERSION_RE.test(p.version)) {
64
+ if (SEMVER_RE.test(p.version)) {
65
+ findings.push({ validatorId: this.id, severity: "info", file, line: p.line, message: `Version "${p.version}" in ${name} is SemVer with a suffix: accepted by denikson's BepInExPack_Valheim, rejected by stock BepInEx 5 ("Skipping type ... because its version is invalid").`, evidence: PLUGIN_EVIDENCE });
66
+ }
67
+ else {
68
+ findings.push({ validatorId: this.id, severity: "block", file, line: p.line, message: `Version "${p.version}" in ${name} is neither a System.Version (2-4 integers) nor SemVer; BepInEx skips the plugin with "its version is invalid".`, evidence: PLUGIN_EVIDENCE });
69
+ }
70
+ }
71
+ if (p.name !== undefined && p.name.trim().length === 0) {
72
+ findings.push({ validatorId: this.id, severity: "warn", file, line: p.line, message: `[BepInPlugin] in ${name} has an empty Name. BepInEx loads it — only a null name is skipped — but Name is the plugin's log source and its display name, so its LogOutput.log lines arrive as "[Info : ]" with nothing to attribute them to, and Jötunn's mismatch panel names it as blank.`, fix: 'Give it a human-readable name, e.g. [BepInPlugin("com.example.mod", "Mod Name", "1.0.0")].', evidence: PLUGIN_EVIDENCE });
73
+ }
74
+ }
75
+ return findings;
76
+ },
77
+ };
78
+ async function manifestDependencies(ctx) {
79
+ const out = [];
80
+ for (const s of ctx.project.sources) {
81
+ if (!s.exists || s.kind !== PACKAGE_SOURCE_KIND)
82
+ continue;
83
+ const file = path.join(s.absPath, "manifest.json");
84
+ let text;
85
+ try {
86
+ text = await fs.readFile(file, "utf8");
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ try {
92
+ const { manifest } = parseThunderstoreManifest(text);
93
+ const packages = (manifest.dependencies ?? []).flatMap((raw) => {
94
+ const parsed = parseDependencyString(raw);
95
+ return parsed ? [{ ...parsed, raw }] : [];
96
+ });
97
+ out.push({ file, packages });
98
+ }
99
+ catch {
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ const LOADER_PACKAGES = new Set(["bepinexpack_valheim", "bepinexpack"]);
105
+ const dependencyCrossCheck = {
106
+ id: "valheim.plugins.dependency-crosscheck",
107
+ game: "valheim",
108
+ groups: ["plugins"],
109
+ modTypes: ["gameplay-tweak", "packaging"],
110
+ summary: "Every hard [BepInDependency] GUID has a matching package in manifest.json's dependencies (by name), and every manifest dependency other than the loader pack is named by some [BepInDependency]; the managers install the manifest's list, BepInEx orders by the attribute's.",
111
+ appliesTo: (target, project) => isCsUnderSource(target, project) && project.sources.some((s) => s.exists && s.kind === PACKAGE_SOURCE_KIND),
112
+ appliesToDescription: `${CS_APPLIES}, when the project also has a "${PACKAGE_SOURCE_KIND}" source`,
113
+ needs: { project: true },
114
+ cost: "edit",
115
+ severity: "warn",
116
+ evidence: DEPENDENCY_EVIDENCE,
117
+ async run(ctx) {
118
+ if (ctx.target.kind !== "file")
119
+ return [];
120
+ const file = ctx.target.path;
121
+ const name = path.basename(file);
122
+ const s = await scan(ctx, file);
123
+ if (s.dependencies.length === 0 && s.plugins.length === 0)
124
+ return [];
125
+ const manifests = await manifestDependencies(ctx);
126
+ if (manifests.length === 0)
127
+ return [];
128
+ const findings = [];
129
+ const allPackages = manifests.flatMap((m) => m.packages.map((p) => ({ ...p, manifest: m.file })));
130
+ for (const d of s.dependencies) {
131
+ if (d.guid === undefined) {
132
+ if (d.unresolved.length > 0)
133
+ findings.push({ validatorId: this.id, severity: "info", file, line: d.line, message: `[BepInDependency] in ${name} names its GUID as '${d.unresolved[0]}', not resolvable from this file, so it was not cross-checked.`, evidence: DEPENDENCY_EVIDENCE });
134
+ continue;
135
+ }
136
+ if (d.kind === "soft")
137
+ continue;
138
+ const match = allPackages.find((p) => packageNameMatchesGuid(p.name, d.guid));
139
+ if (!match) {
140
+ findings.push({
141
+ validatorId: this.id,
142
+ severity: "warn",
143
+ file,
144
+ line: d.line,
145
+ message: `${name} declares a hard [BepInDependency] on "${d.guid}", and no dependency in manifest.json (${allPackages.map((p) => p.raw).join(", ") || "none"}) has a package name matching it; if it is a Thunderstore package the managers will not install it, and BepInEx then logs "Could not load [...] because it has missing dependencies: ${d.guid}".`,
146
+ fix: "Add the package's {team}-{package}-{version} string to manifest.json's dependencies (or make the attribute a SoftDependency if the mod works without it). The match is by name and may miss a package whose name differs from its GUID.",
147
+ evidence: DEPENDENCY_EVIDENCE,
148
+ related: manifests.map((m) => m.file),
149
+ });
150
+ }
151
+ else if (d.minimumVersion !== undefined && parseVersion(d.minimumVersion) !== undefined && parseVersion(match.version) !== undefined) {
152
+ if (compareVersions(match.version, d.minimumVersion) < 0) {
153
+ findings.push({ validatorId: this.id, severity: "warn", file, line: d.line, message: `${name} requires "${d.guid}" at ${d.minimumVersion} or newer, but manifest.json pins ${match.raw}, which is older; the managers install the pinned version and BepInEx refuses the plugin.`, fix: `Raise the manifest dependency to ${match.team}-${match.name}-${d.minimumVersion} or lower the attribute's minimum.`, evidence: DEPENDENCY_EVIDENCE, related: [match.manifest] });
154
+ }
155
+ }
156
+ }
157
+ if (s.plugins.length > 0) {
158
+ const guids = s.dependencies.map((d) => d.guid).filter((g) => g !== undefined);
159
+ for (const p of allPackages) {
160
+ if (LOADER_PACKAGES.has(p.name.toLowerCase()))
161
+ continue;
162
+ if (guids.some((g) => packageNameMatchesGuid(p.name, g)))
163
+ continue;
164
+ findings.push({
165
+ validatorId: this.id,
166
+ severity: "info",
167
+ file: p.manifest,
168
+ message: `manifest.json depends on ${p.raw}, and no [BepInDependency] in ${name} names a GUID matching "${p.name}"; the managers install it, but BepInEx will not order this plugin after it, so it may run before its dependency has loaded.`,
169
+ fix: `Add [BepInDependency("<its GUID>")] to the plugin class, or drop the manifest entry if it is not a plugin dependency. The match is by name and may miss a package whose name differs from its GUID.`,
170
+ evidence: DEPENDENCY_EVIDENCE,
171
+ related: [file],
172
+ });
173
+ }
174
+ }
175
+ return findings;
176
+ },
177
+ };
178
+ const harmony = {
179
+ id: "valheim.plugins.harmony-calls",
180
+ game: "valheim",
181
+ groups: ["plugins"],
182
+ modTypes: ["gameplay-tweak"],
183
+ summary: "UnpatchAll() with no id (or null) is never called: it removes every mod's patches and the community records save corruption from it. Parameterless PatchAll() is noted: its assembly comes from the calling stack frame, which inlining can get wrong.",
184
+ appliesTo: isCsUnderSource,
185
+ appliesToDescription: CS_APPLIES,
186
+ needs: { project: true },
187
+ cost: "edit",
188
+ severity: "block",
189
+ evidence: HARMONY_EVIDENCE,
190
+ async run(ctx) {
191
+ if (ctx.target.kind !== "file")
192
+ return [];
193
+ const file = ctx.target.path;
194
+ const name = path.basename(file);
195
+ const s = await scan(ctx, file);
196
+ const findings = [];
197
+ for (const u of s.unpatchAllWithoutId) {
198
+ findings.push({ validatorId: this.id, severity: "block", file, line: u.line, message: `${name} calls UnpatchAll() with no Harmony id: that removes every plugin's patches, BepInEx logs "UnpatchAll has been called - This will remove ALL HARMONY PATCHES.", and the Valheim wiki records save corruption from it.`, fix: 'Call harmony.UnpatchSelf() or UnpatchAll("your.harmony.id"), and not on game shutdown at all.', evidence: HARMONY_EVIDENCE });
199
+ }
200
+ for (const p of s.patchAllParameterless) {
201
+ findings.push({ validatorId: this.id, severity: "info", file, line: p.line, message: `${name} calls PatchAll() with no assembly; Harmony resolves it from the calling stack frame and its own documentation warns that inlining can pick the wrong one.`, fix: "Prefer PatchAll(Assembly.GetExecutingAssembly()) or Harmony.CreateAndPatchAll(typeof(Plugin).Assembly, id).", evidence: HARMONY_EVIDENCE });
202
+ }
203
+ return findings;
204
+ },
205
+ };
206
+ const networkCompatibility = {
207
+ id: "valheim.plugins.network-compatibility",
208
+ game: "valheim",
209
+ groups: ["plugins"],
210
+ modTypes: ["gameplay-tweak"],
211
+ summary: `[NetworkCompatibility] names a CompatibilityLevel (${COMPATIBILITY_LEVELS.join(", ")}) and a VersionStrictness (${VERSION_STRICTNESS.join(", ")}); a mod enforcing EveryoneMustHaveMod or ServerMustHaveMod needs deploying to the server too.`,
212
+ appliesTo: isCsUnderSource,
213
+ appliesToDescription: CS_APPLIES,
214
+ needs: { project: true },
215
+ cost: "edit",
216
+ severity: "warn",
217
+ evidence: NETWORK_EVIDENCE,
218
+ async run(ctx) {
219
+ if (ctx.target.kind !== "file")
220
+ return [];
221
+ const file = ctx.target.path;
222
+ const name = path.basename(file);
223
+ const s = await scan(ctx, file);
224
+ const findings = [];
225
+ for (const n of s.networkCompatibility) {
226
+ if (n.level !== undefined && !COMPATIBILITY_LEVELS.includes(n.level)) {
227
+ findings.push({ validatorId: this.id, severity: "warn", file, line: n.line, message: `[NetworkCompatibility] in ${name} names CompatibilityLevel.${n.level}, which is not a member (${COMPATIBILITY_LEVELS.join(", ")}).`, evidence: NETWORK_EVIDENCE });
228
+ }
229
+ if (n.strictness !== undefined && !VERSION_STRICTNESS.includes(n.strictness)) {
230
+ findings.push({ validatorId: this.id, severity: "warn", file, line: n.line, message: `[NetworkCompatibility] in ${name} names VersionStrictness.${n.strictness}, which is not a member (${VERSION_STRICTNESS.join(", ")}).`, evidence: NETWORK_EVIDENCE });
231
+ }
232
+ if (n.level === "EveryoneMustHaveMod" || n.level === "ServerMustHaveMod") {
233
+ const clientOnly = s.bepInProcesses.some((p) => p.name?.toLowerCase() === "valheim.exe") && !s.bepInProcesses.some((p) => p.name?.toLowerCase() === "valheim_server.exe");
234
+ if (clientOnly) {
235
+ findings.push({ validatorId: this.id, severity: "warn", file, line: n.line, message: `${name} enforces CompatibilityLevel.${n.level} but its [BepInProcess] restricts it to valheim.exe, so it can never be loaded on the server it requires itself on; every client would be refused with Jötunn's "Missing Mods" panel.`, fix: "Drop the [BepInProcess] restriction or add one for valheim_server.exe.", evidence: NETWORK_EVIDENCE });
236
+ }
237
+ else {
238
+ findings.push({ validatorId: this.id, severity: "info", file, line: n.line, message: `${name} enforces CompatibilityLevel.${n.level}: the dedicated server must run this exact plugin (to ${n.strictness ?? "the declared"} strictness) or clients are disconnected into Jötunn's mismatch panel; deploy it to the server install too.`, evidence: NETWORK_EVIDENCE });
239
+ }
240
+ }
241
+ }
242
+ return findings;
243
+ },
244
+ };
245
+ function isCfgUnderSource(target, project) {
246
+ if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ".cfg")
247
+ return false;
248
+ return project.sources.some((s) => s.exists && isWithin(s.absPath, target.path));
249
+ }
250
+ const cfgSyntax = {
251
+ id: "valheim.plugins.config-syntax",
252
+ game: "valheim",
253
+ groups: ["plugins"],
254
+ modTypes: ["gameplay-tweak", "packaging"],
255
+ summary: "A shipped .cfg is in BepInEx's ConfigFile shape ([Section] headers, Key = Value, # comments) with no duplicate key in a section; a plugin config is named <GUID>.cfg.",
256
+ appliesTo: isCfgUnderSource,
257
+ appliesToDescription: "every .cfg under a project source",
258
+ needs: { project: true },
259
+ cost: "edit",
260
+ severity: "warn",
261
+ evidence: CFG_EVIDENCE,
262
+ async run(ctx) {
263
+ if (ctx.target.kind !== "file")
264
+ return [];
265
+ const file = ctx.target.path;
266
+ const name = path.basename(file);
267
+ const s = scanCfg(await ctx.readText(file));
268
+ const findings = [];
269
+ for (const m of s.malformed) {
270
+ findings.push({ validatorId: this.id, severity: "warn", file, line: m.line, message: `${name} line ${m.line} is neither a [Section], a Key = Value pair nor a # comment: "${m.text}"; BepInEx's ConfigFile skips it.`, evidence: CFG_EVIDENCE });
271
+ }
272
+ for (const d of s.duplicates) {
273
+ findings.push({ validatorId: this.id, severity: "warn", file, line: d.lines[d.lines.length - 1], message: `${name} sets "${d.key}" ${d.lines.length} times in [${d.section || "(no section)"}] (lines ${d.lines.join(", ")}); only one value survives.`, evidence: CFG_EVIDENCE });
274
+ }
275
+ if (s.entries.some((e) => e.section === "")) {
276
+ findings.push({ validatorId: this.id, severity: "warn", file, line: s.entries.find((e) => e.section === "").line, message: `${name} has Key = Value lines before any [Section]; BepInEx's ConfigFile keys every entry by section, so these belong to none.`, evidence: CFG_EVIDENCE });
277
+ }
278
+ if (name.toLowerCase() !== "bepinex.cfg") {
279
+ const guid = name.slice(0, -".cfg".length);
280
+ if (!BEPINPLUGIN_GUID_RE.test(guid) || !guid.includes(".")) {
281
+ findings.push({ validatorId: this.id, severity: "info", file, message: `${name} is not named like a plugin config: BepInEx writes a plugin's config as <GUID>.cfg (reverse-domain GUID), and a shipped file under config/ only takes effect under that name.`, evidence: CFG_EVIDENCE });
282
+ }
283
+ }
284
+ return findings;
285
+ },
286
+ };
287
+ export const PLUGIN_VALIDATORS = [bepInPlugin, dependencyCrossCheck, harmony, networkCompatibility, cfgSyntax];
@@ -0,0 +1,169 @@
1
+ import * as path from "node:path";
2
+ import { walk } from "../../../core/fsutil.js";
3
+ import { scanReferences, translationKeys } from "../references.js";
4
+ import { SOURCE_SOURCE_KIND } from "./plugins.js";
5
+ const EVIDENCE = [
6
+ "valheim.jotunn.library.references-are-by-prefab-name-and-token",
7
+ "valheim.jotunn.library.jotunndoc-publishes-a-vanilla-index",
8
+ "valheim.jotunn.library.jotunndoc-table-shapes",
9
+ ];
10
+ const APPLIES = `every .cs file under a project source of kind "${SOURCE_SOURCE_KIND}"`;
11
+ const MAX_PER_FILE = 30;
12
+ function isWithin(parent, child) {
13
+ const rel = path.relative(parent, child);
14
+ return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
15
+ }
16
+ function isCsUnderSource(target, project) {
17
+ if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ".cs")
18
+ return false;
19
+ return project.sources.some((s) => s.exists && s.kind === SOURCE_SOURCE_KIND && isWithin(s.absPath, target.path));
20
+ }
21
+ async function projectDeclarations(ctx) {
22
+ const out = { prefabs: new Set(), tokens: new Set(), pieceTables: new Set() };
23
+ for (const s of ctx.project.sources) {
24
+ if (!s.exists)
25
+ continue;
26
+ const { files } = await walk(s.absPath);
27
+ for (const rel of files) {
28
+ const lower = rel.toLowerCase();
29
+ const full = path.join(s.absPath, ...rel.split("/"));
30
+ if (s.kind === SOURCE_SOURCE_KIND && lower.endsWith(".cs")) {
31
+ try {
32
+ const scan = scanReferences(await ctx.readText(full));
33
+ for (const p of scan.declares.prefabs)
34
+ out.prefabs.add(p.toLowerCase());
35
+ for (const t of scan.declares.tokens)
36
+ out.tokens.add(t.toLowerCase());
37
+ for (const t of scan.declares.pieceTables)
38
+ out.pieceTables.add(t.toLowerCase());
39
+ }
40
+ catch {
41
+ }
42
+ }
43
+ else if (lower.endsWith(".json") && /(^|\/)translations\//.test(lower)) {
44
+ try {
45
+ for (const k of translationKeys(await ctx.readText(full)))
46
+ out.tokens.add(k.toLowerCase());
47
+ }
48
+ catch {
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function declaredByProject(declared, ref) {
56
+ const value = ref.value.toLowerCase();
57
+ switch (ref.kind) {
58
+ case "token":
59
+ return declared.tokens.has(value.replace(/^\$/, ""));
60
+ case "prefab":
61
+ case "requirement":
62
+ return declared.prefabs.has(value);
63
+ case "piece_table":
64
+ return declared.pieceTables.has(value) || declared.prefabs.has(value);
65
+ case "asset":
66
+ return false;
67
+ }
68
+ }
69
+ async function inIndex(ctx, ref) {
70
+ switch (ref.kind) {
71
+ case "prefab":
72
+ case "requirement": {
73
+ const r = await ctx.lookup("entry", { name: ref.value, limit: 1 });
74
+ return r.count > 0;
75
+ }
76
+ case "piece_table": {
77
+ const byName = await ctx.lookup("entry", { kind: "piece_table", name: ref.value, limit: 1 });
78
+ if (byName.count > 0)
79
+ return true;
80
+ const tables = await ctx.lookup("entry", { kind: "piece_table", limit: 200 });
81
+ return tables.rows.some((row) => {
82
+ const data = row.data;
83
+ return data !== undefined && Object.entries(data).some(([column, value]) => /alias/i.test(column) && value.toLowerCase() === ref.value.toLowerCase());
84
+ });
85
+ }
86
+ case "token": {
87
+ const r = await ctx.lookup("token", { token: ref.value, limit: 1 });
88
+ return r.count > 0;
89
+ }
90
+ case "asset": {
91
+ const r = await ctx.lookup("asset", { id: ref.value });
92
+ return r.count > 0;
93
+ }
94
+ }
95
+ }
96
+ const KIND_WORDS = {
97
+ prefab: "prefab",
98
+ requirement: "recipe item",
99
+ piece_table: "piece table",
100
+ token: "localization token",
101
+ asset: "AssetID",
102
+ };
103
+ function consequence(ref) {
104
+ switch (ref.kind) {
105
+ case "prefab":
106
+ return "Jötunn's tutorials say a clone source must be an existing prefab; a name that resolves nowhere leaves the item or piece unregistered at runtime.";
107
+ case "requirement":
108
+ return "Jötunn's tutorials say a recipe's crafted item and its requirements must name existing item prefabs; a name that resolves nowhere makes the recipe unfulfillable or drops it when Jötunn resolves it.";
109
+ case "piece_table":
110
+ return "a piece whose table does not exist is never shown in any build menu.";
111
+ case "token":
112
+ return "Jötunn's localization falls back to English and then to Valheim's own Translation class, so a token neither the mod nor the game defines reaches the player unresolved.";
113
+ case "asset":
114
+ return "Iron Gate's soft-reference layer keys on the AssetID, which \"will always stay the same for each asset even if it's moved or renamed\", so an ID no indexed asset carries names nothing in the vanilla data.";
115
+ }
116
+ }
117
+ const references = {
118
+ id: "valheim.plugins.vanilla-references",
119
+ game: "valheim",
120
+ groups: ["plugins"],
121
+ modTypes: ["gameplay-tweak", "item", "weapon", "armor"],
122
+ summary: "Every prefab a CustomItem/CustomPiece clones or GetPrefab fetches, every recipe requirement item, every literal piece table, every $token and every 32-hex AssetID in the plugin's source resolves in the vanilla index or is declared by the project; each miss is a warning, since the name may belong to another mod or the dumps may lag the game.",
123
+ appliesTo: isCsUnderSource,
124
+ appliesToDescription: APPLIES,
125
+ needs: { project: true, index: true },
126
+ cost: "build",
127
+ severity: "warn",
128
+ evidence: EVIDENCE,
129
+ async run(ctx) {
130
+ if (ctx.target.kind !== "file")
131
+ return [];
132
+ const file = ctx.target.path;
133
+ const name = path.basename(file);
134
+ const scan = scanReferences(await ctx.readText(file));
135
+ if (scan.refs.length === 0)
136
+ return [];
137
+ const declared = await projectDeclarations(ctx);
138
+ const findings = [];
139
+ const seen = new Set();
140
+ const refs = [...scan.refs].sort((a, b) => a.line - b.line);
141
+ for (const [i, ref] of refs.entries()) {
142
+ const key = `${ref.kind}:${ref.value.toLowerCase()}`;
143
+ if (seen.has(key))
144
+ continue;
145
+ seen.add(key);
146
+ if (declaredByProject(declared, ref))
147
+ continue;
148
+ if (await inIndex(ctx, ref))
149
+ continue;
150
+ findings.push({
151
+ validatorId: this.id,
152
+ severity: "warn",
153
+ file,
154
+ line: ref.line,
155
+ message: `${name} references ${KIND_WORDS[ref.kind]} "${ref.value}" (${ref.via}), which is neither in the vanilla index nor declared by this project; ${consequence(ref)}`,
156
+ fix: ref.kind === "token"
157
+ ? "Register the token with AddTranslation or a Translations/<Language>/*.json file, or use a vanilla token from the index (lookup kind token). The index carries the Token column of the JotunnDoc object lists, not Valheim's full translation table, so a vanilla token that belongs to no prefab, item or piece (a $menu_* or $msg_* string) warns here and is fine."
158
+ : "Check the spelling against the index (lookup kind entry, or asset), or declare the name in this project; if it belongs to another installed mod, this warning is expected.",
159
+ evidence: EVIDENCE,
160
+ });
161
+ if (findings.length >= MAX_PER_FILE && i < refs.length - 1) {
162
+ findings.push({ validatorId: this.id, severity: "info", file, message: `More unresolved references in ${name} than this rule lists (${MAX_PER_FILE}); fix these first.`, evidence: EVIDENCE });
163
+ break;
164
+ }
165
+ }
166
+ return findings;
167
+ },
168
+ };
169
+ export const REFERENCE_VALIDATORS = [references];
@@ -0,0 +1,166 @@
1
+ game: baldursgate3
2
+ topic: animations.gr2
3
+ title: GR2 animation export (GLTF vs Collada) and Toolkit animation registration
4
+ facts:
5
+ - id: bg3.animations.gr2.export-via-gltf-not-collada-for-animations
6
+ claim: When exporting a baked skeleton animation to .GR2 with Norbyte's DOS2/BG3 Collada Exporter
7
+ add-on, the documented path is "export via GLTF" specifically, "not via Collada" — the add-on
8
+ supports both, but the GLTF path is the one that correctly carries animation-bearing meshes
9
+ for BG3.
10
+ status: community
11
+ source: wiki.bg3.community/en/Tutorials/Animations/CustomAnimationsBlender §How to correctly bake
12
+ and export your animation
13
+ tags:
14
+ - animation
15
+ - gr2
16
+ - gltf
17
+ - collada
18
+ - export
19
+ - id: bg3.animations.gr2.export-only-skeleton-not-control-rig-or-meshes
20
+ claim: "\"You only want to export the skeleton rig, not the control rig or any of the meshes\",
21
+ because \"While you do your animating on the control rig, the skeleton does not receive any
22
+ keyframes, it's being constrained (puppeteered) by the control rig\" — so the Action must
23
+ first be baked onto the skeleton (\"Go into Pose > Animation > Bake Action\") before export."
24
+ status: community
25
+ source: wiki.bg3.community/en/Tutorials/Animations/CustomAnimationsBlender §How to correctly bake
26
+ and export your animation
27
+ tags:
28
+ - animation
29
+ - gr2
30
+ - bake-action
31
+ - control-rig
32
+ - export
33
+ - id: bg3.animations.gr2.bake-action-clear-constraints-disconnects-rig
34
+ claim: Baking Action on a skeleton rig with "clear constraints" checked disconnects the skeleton
35
+ from the control rig afterward (moving the control rig no longer moves the skeleton); leaving
36
+ that option unchecked keeps the skeleton connected to the control rig after baking.
37
+ status: community
38
+ source: wiki.bg3.community/en/Tutorials/Animations/CustomAnimationsBlender §How to correctly bake
39
+ and export your animation
40
+ tags:
41
+ - animation
42
+ - bake-action
43
+ - control-rig
44
+ - id: bg3.animations.gr2.gr2-bones-are-joints-not-blender-bones
45
+ claim: GR2 bones function as single-point joints (like Maya), not two-ended Blender bones with a
46
+ head and tail; Blender's importer guesses a tail position using Blender's own primary axis
47
+ convention (Y) versus GR2/Maya's (X), which makes imported bones look "sideways" in Blender —
48
+ this is expected and exports back correctly, since only the joint/bone head position actually
49
+ matters.
50
+ status: community
51
+ source: wiki.bg3.community/en/Tutorials/Animations/CustomAnimationsBlender §How to correctly bake
52
+ and export your animation
53
+ tags:
54
+ - animation
55
+ - gr2
56
+ - bones
57
+ - joints
58
+ - blender
59
+ - id: bg3.animations.gr2.filename-identity-required-between-custom-and-toolkit-gr2
60
+ claim: "\"It's important that your custom animation .GR2 is named the same as the file in the
61
+ Toolkit\": you open the animation resource's \"Source File\" path (minus the filename), then
62
+ \"replace the vanilla .GR2 file with your own in the folder you just opened up\"."
63
+ status: community
64
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §Adding Poses to the
65
+ _Base
66
+ tags:
67
+ - animation
68
+ - gr2
69
+ - toolkit
70
+ - filename-identity
71
+ - silent-failure
72
+ - id: bg3.animations.gr2.body-type-base-fallback-to-hum-m-base
73
+ claim: If a specific body-type _Base AnimationSetResource doesn't have an animation registered, the
74
+ game falls back to whatever is registered in HUM_M_Base — "everyone gets to use the HUM_M_Base
75
+ ones" — so an animation added only to HUM_F_Base is usable only by BT1 (that body type)
76
+ characters, while one added to HUM_M_Base is available to all body types that don't have their
77
+ own override.
78
+ status: community
79
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §Adding Poses to the
80
+ _Base ("Everything will default back to the HUM_M_Base if the other bases don't have an
81
+ animation set into them, (everyone gets to use the HUM_M_Base ones) if something is just in
82
+ the HUM_F_Base then only the BT1 characters can use those")
83
+ tags:
84
+ - animation
85
+ - animationset
86
+ - hum-m-base
87
+ - fallback
88
+ - id: bg3.animations.gr2.photomode-pose-mods-override-each-other
89
+ claim: Photomode pose/animation mods override one another rather than stacking, so only one such mod
90
+ can be active in a load order at a time — a constraint worth stating explicitly when
91
+ publishing.
92
+ status: community
93
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit ("Currently,
94
+ photomode pose/animation mods override one another, so you can only have one in your load
95
+ order at a time")
96
+ tags:
97
+ - animation
98
+ - photomode
99
+ - load-order
100
+ - compatibility
101
+ - id: bg3.animations.gr2.larian-packs-multiple-poses-per-animation-file
102
+ claim: '"Instead of having one animation file for each static pose, Larian puts each pose in a
103
+ collection into one animation file. The timing for each pose is then set in the Object UUID
104
+ Editor."'
105
+ status: community
106
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §2.4 EmoteCollections
107
+ tags:
108
+ - animation
109
+ - toolkit
110
+ - pose-collection
111
+ - id: bg3.animations.gr2.shortname-required-per-individual-pose
112
+ claim: In the Object UUID Editor's ShortNames, "you want to add an entry for each pose that you
113
+ have. Not for a collection/pack, but for every single animation" — so a file holding 10 static
114
+ poses needs 10 ShortName entries. One pose adjusted per body type can reuse a single ShortName
115
+ across each body-type's _Base ("HUM_F_Hugs_BT1 and the same pose adjusted for hight for
116
+ HUM_M_Hugs_BT1 can share the same shortname").
117
+ status: community
118
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §2.2 ShortNames
119
+ tags:
120
+ - animation
121
+ - shortnames
122
+ - toolkit
123
+ - uuid-object-editor
124
+ - id: bg3.animations.gr2.emoteposes-vs-emoteanimations-distinction
125
+ claim: A static pose is registered under EmotePoses; a moving animation follows "these same steps
126
+ but instead of adding 'EmotePoses', add 'EmoteAnimations' instead. The only difference is that
127
+ is doesn't have 'Timing' in the tab."
128
+ status: community
129
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §2.3 EmotePoses and
130
+ EmoteAnimations
131
+ tags:
132
+ - animation
133
+ - emoteposes
134
+ - emoteanimations
135
+ - toolkit
136
+ - id: bg3.animations.gr2.toolkit-restart-needed-after-gr2-replacement
137
+ claim: After manually replacing a vanilla .GR2 file on disk with a custom one of the same name, the
138
+ Toolkit must be closed and reopened for the new animation to show correctly in the project
139
+ folder — it will not pick up the swapped file while still running.
140
+ status: community
141
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit ("you have to close
142
+ down the Toolkit and open it again in order for your animations to show properly in the
143
+ project folder")
144
+ tags:
145
+ - animation
146
+ - toolkit
147
+ - gr2
148
+ - restart-required
149
+ - id: bg3.animations.gr2.publish-local-required-before-bg3mm-load-order
150
+ claim: After setting up an animation mod's project settings (Project > Project Settings, Ctrl+P) and
151
+ filling the mandatory fields plus author, the Toolkit's "Publish Local" step is what generates
152
+ the .pak into the local Mods folder, which is then added to a load order and exported to the
153
+ game in BG3 Mod Manager — this is a Toolkit-only publish step distinct from publishing to
154
+ mod.io.
155
+ status: community
156
+ source: wiki.bg3.community/en/Tutorials/Animations/Adding-Animations-In-Toolkit §Exporting Your Mod
157
+ ("Publish Local (automatically adds the mod to your Mods folder)")
158
+ tags:
159
+ - animation
160
+ - toolkit
161
+ - publish-local
162
+ - bg3mm
163
+ detail: Larian's own publishing doc describes Publish Local as writing a .pak "without uploading it
164
+ to mod.io" to a location you choose — see
165
+ bg3.publishing.toolkit-modio.publish-local-vs-publish-to-modio; this page's phrasing is the
166
+ animation workflow's shorthand for the same button.