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,173 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ export const PAK_MAGIC = 0x5a6f12e1;
4
+ export const PAK_VERSION_MAJOR_NAMES = {
5
+ 0: "Unknown",
6
+ 1: "Initial",
7
+ 2: "NoTimestamps",
8
+ 3: "CompressionEncryption",
9
+ 4: "IndexEncryption",
10
+ 5: "RelativeChunkOffsets",
11
+ 6: "DeleteRecords",
12
+ 7: "EncryptionKeyGuid",
13
+ 8: "FNameBasedCompression",
14
+ 9: "FrozenIndex",
15
+ 10: "PathHashIndex",
16
+ 11: "Fnv64BugFix",
17
+ };
18
+ export const GAME_MOUNT_POINT = "../../../";
19
+ const FOOTER_SCAN_BYTES = 256;
20
+ export class PakError extends Error {
21
+ reason;
22
+ constructor(reason, message) {
23
+ super(message);
24
+ this.reason = reason;
25
+ this.name = "PakError";
26
+ }
27
+ }
28
+ export function parsePakFooter(tail, fileSize) {
29
+ if (tail.length < 44)
30
+ throw new PakError("too-short", `only ${tail.length} bytes; a pak footer is at least 44`);
31
+ const view = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);
32
+ const tailStart = fileSize - tail.length;
33
+ let best;
34
+ for (let i = 0; i + 24 <= tail.length; i++) {
35
+ if (view.getUint32(i, true) !== PAK_MAGIC)
36
+ continue;
37
+ const versionMajor = view.getUint32(i + 4, true);
38
+ if (versionMajor < 1 || versionMajor > 11)
39
+ continue;
40
+ const indexOffset = Number(view.getBigUint64(i + 8, true));
41
+ const indexSize = Number(view.getBigUint64(i + 16, true));
42
+ if (versionMajor >= 4 && i < 1)
43
+ continue;
44
+ const magicOffset = tailStart + i;
45
+ const footerStart = magicOffset - (versionMajor >= 7 ? 16 : 0) - (versionMajor >= 4 ? 1 : 0);
46
+ if (indexOffset >= footerStart || indexOffset + indexSize > footerStart)
47
+ continue;
48
+ const encryptedIndex = versionMajor >= 4 ? tail[i - 1] === 1 : false;
49
+ best = { versionMajor, versionName: PAK_VERSION_MAJOR_NAMES[versionMajor] ?? String(versionMajor), indexOffset, indexSize, encryptedIndex, fileSize, magicOffset };
50
+ }
51
+ if (!best)
52
+ throw new PakError("no-magic", "no pak footer magic (0x5A6F12E1) with a plausible index behind it in the last bytes of the file");
53
+ return best;
54
+ }
55
+ export async function readPakFooter(file) {
56
+ const handle = await fs.open(file, "r");
57
+ try {
58
+ const { size } = await handle.stat();
59
+ const length = Math.min(FOOTER_SCAN_BYTES, size);
60
+ const buf = new Uint8Array(length);
61
+ const { bytesRead } = await handle.read(buf, 0, length, size - length);
62
+ return parsePakFooter(buf.subarray(0, bytesRead), size);
63
+ }
64
+ finally {
65
+ await handle.close();
66
+ }
67
+ }
68
+ export function parseFString(bytes, at = 0) {
69
+ if (bytes.length < at + 4)
70
+ throw new PakError("bad-index", "index shorter than a string length");
71
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
72
+ const len = view.getInt32(at, true);
73
+ if (len === 0)
74
+ return { value: "", length: 4 };
75
+ if (len < 0) {
76
+ const units = -len;
77
+ if (bytes.length < at + 4 + units * 2)
78
+ throw new PakError("bad-index", "UTF-16 string runs past the index");
79
+ const chars = [];
80
+ for (let i = 0; i < units; i++) {
81
+ const c = view.getUint16(at + 4 + i * 2, true);
82
+ if (c === 0)
83
+ break;
84
+ chars.push(c);
85
+ }
86
+ return { value: String.fromCharCode(...chars), length: 4 + units * 2 };
87
+ }
88
+ if (len > 4096)
89
+ throw new PakError("bad-index", `string length ${len} is not a mount point; the index is probably encrypted or damaged`);
90
+ if (bytes.length < at + 4 + len)
91
+ throw new PakError("bad-index", "string runs past the index");
92
+ const raw = bytes.subarray(at + 4, at + 4 + len);
93
+ const nul = raw.indexOf(0);
94
+ return { value: Buffer.from(nul >= 0 ? raw.subarray(0, nul) : raw).toString("utf8"), length: 4 + len };
95
+ }
96
+ export async function readPakInfo(file) {
97
+ const footer = await readPakFooter(file);
98
+ if (footer.encryptedIndex)
99
+ return { ...footer };
100
+ const handle = await fs.open(file, "r");
101
+ try {
102
+ const length = Math.min(4 + 4096, footer.indexSize);
103
+ const buf = new Uint8Array(length);
104
+ const { bytesRead } = await handle.read(buf, 0, length, footer.indexOffset);
105
+ return { ...footer, mountPoint: parseFString(buf.subarray(0, bytesRead)).value };
106
+ }
107
+ finally {
108
+ await handle.close();
109
+ }
110
+ }
111
+ export const UTOC_MAGIC = "-==--==--==--==-";
112
+ export const UTOC_VERSION_NAMES = {
113
+ 0: "Invalid",
114
+ 1: "Initial",
115
+ 2: "DirectoryIndex",
116
+ 3: "PartitionSize",
117
+ 4: "PerfectHash",
118
+ 5: "PerfectHashWithOverflow",
119
+ 6: "OnDemandMetaData",
120
+ 7: "RemovedOnDemandMetaData",
121
+ 8: "ReplaceIoChunkHashWithIoHash",
122
+ };
123
+ export const UTOC_HEADER_READ = 28;
124
+ export class UtocError extends Error {
125
+ reason;
126
+ constructor(reason, message) {
127
+ super(message);
128
+ this.reason = reason;
129
+ this.name = "UtocError";
130
+ }
131
+ }
132
+ export function parseUtocHeader(bytes) {
133
+ if (bytes.length < UTOC_HEADER_READ)
134
+ throw new UtocError("too-short", `only ${bytes.length} bytes; a TOC header starts with ${UTOC_HEADER_READ}`);
135
+ const magic = Buffer.from(bytes.subarray(0, 16)).toString("latin1");
136
+ if (magic !== UTOC_MAGIC)
137
+ throw new UtocError("bad-magic", `first 16 bytes are ${JSON.stringify(magic)}, not the IoStore TOC magic`);
138
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
139
+ const version = bytes[16];
140
+ return { version, versionName: UTOC_VERSION_NAMES[version] ?? String(version), headerSize: view.getUint32(20, true), entryCount: view.getUint32(24, true) };
141
+ }
142
+ export async function readUtocHeader(file) {
143
+ const handle = await fs.open(file, "r");
144
+ try {
145
+ const buf = new Uint8Array(UTOC_HEADER_READ);
146
+ const { bytesRead } = await handle.read(buf, 0, UTOC_HEADER_READ, 0);
147
+ return parseUtocHeader(buf.subarray(0, bytesRead));
148
+ }
149
+ finally {
150
+ await handle.close();
151
+ }
152
+ }
153
+ export const TRIPLET_EXTENSIONS = [".pak", ".utoc", ".ucas"];
154
+ export async function tripletsIn(dir) {
155
+ const entries = await fs.readdir(dir, { withFileTypes: true });
156
+ const byStem = new Map();
157
+ for (const e of entries) {
158
+ if (!e.isFile())
159
+ continue;
160
+ const ext = path.extname(e.name).toLowerCase();
161
+ if (!TRIPLET_EXTENSIONS.includes(ext))
162
+ continue;
163
+ const stem = e.name.slice(0, -ext.length);
164
+ const key = stem.toLowerCase();
165
+ const t = byStem.get(key) ?? { stem, present: {} };
166
+ t.present[ext] = path.join(dir, e.name);
167
+ byStem.set(key, t);
168
+ }
169
+ return [...byStem.values()];
170
+ }
171
+ export function hasPrioritySuffix(stem) {
172
+ return /_p$/i.test(stem);
173
+ }
@@ -0,0 +1,151 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { localAppData, pathExists } from "../../core/fsutil.js";
4
+ import { findInSteam } from "../../core/steam.js";
5
+ import { dedupeByRoot, logs, roots } from "../../core/surfaceutil.js";
6
+ import { subnautica2Compat } from "./compat.js";
7
+ import { SIGNATURES } from "./logs.js";
8
+ import { PACKAGING_VALIDATORS } from "./validators/packaging.js";
9
+ export const PROJECT_DIR = "Subnautica2";
10
+ export const SHIPPING_EXE = path.join(PROJECT_DIR, "Binaries", "Win64", "Subnautica2-Win64-Shipping.exe");
11
+ export const subnautica2 = {
12
+ id: "subnautica2",
13
+ name: "Subnautica 2",
14
+ aliases: ["subnautica2", "sn2"],
15
+ summary: "Unreal Engine 5.6 (IoStore). Pak mods under Content/Paks/~mods, blueprint mods under " +
16
+ "LogicMods, UE4SS Lua/C++ mods, and loader-free DLC plugins under Mods/; no official mod support.",
17
+ async detect() {
18
+ const installs = [];
19
+ const override = process.env.MODWRIGHT_SUBNAUTICA2_ROOT;
20
+ if (override && (await pathExists(override))) {
21
+ installs.push({ gameId: "subnautica2", root: override, source: "env" });
22
+ }
23
+ for (const root of await findInSteam(["Subnautica2"])) {
24
+ if (await pathExists(path.join(root, SHIPPING_EXE))) {
25
+ installs.push({ gameId: "subnautica2", root, source: "steam" });
26
+ }
27
+ }
28
+ return dedupeByRoot(installs);
29
+ },
30
+ modRoots(install) {
31
+ const project = path.join(install.root, PROJECT_DIR);
32
+ const paks = path.join(project, "Content", "Paks");
33
+ return roots([
34
+ {
35
+ id: "paks",
36
+ label: "Pak mods (~mods)",
37
+ path: path.join(paks, "~mods"),
38
+ layout: "file",
39
+ extensions: [".pak", ".utoc", ".ucas"],
40
+ ordered: true,
41
+ conflictScope: "opaque",
42
+ notes: "Loaded in name order, later wins; a _P suffix forces priority. On this IoStore-only " +
43
+ "game a mod is complete only as a .pak + .utoc + .ucas triplet with mount point " +
44
+ "../../../; a lone legacy .pak mounts and delivers nothing. The folder does not exist " +
45
+ "on a fresh install and is a community convention no source confirms this game honours.",
46
+ },
47
+ {
48
+ id: "logicmods",
49
+ label: "Blueprint mods (LogicMods)",
50
+ path: path.join(paks, "LogicMods"),
51
+ layout: "directory",
52
+ conflictScope: "opaque",
53
+ notes: "One folder per mod holding the full triplet named after the folder (the modkit's " +
54
+ "layout, unlike generic UE4SS docs' loose paks). Needs UE4SS's BPModLoaderMod.",
55
+ },
56
+ {
57
+ id: "ue4ss",
58
+ label: "UE4SS mods",
59
+ path: path.join(project, "Binaries", "Win64", "ue4ss", "Mods"),
60
+ layout: "directory",
61
+ ordered: true,
62
+ conflictScope: "isolated",
63
+ notes: "Each subdirectory is one mod with Scripts/main.lua and/or dlls/main.dll; enablement " +
64
+ "is mods.txt ('ModName : 1') plus the enabled.txt convention. This is the experimental " +
65
+ "UE4SS layout; a stable 3.0.x UE4SS keeps Mods/ directly under Binaries/Win64 and does " +
66
+ "not support Unreal 5.6.",
67
+ },
68
+ {
69
+ id: "dlcplugins",
70
+ label: "DLC-style content plugins (Mods/)",
71
+ path: path.join(project, "Mods"),
72
+ layout: "directory",
73
+ conflictScope: "isolated",
74
+ notes: "Content-only plugins the engine loads with no external loader (the modkit's route). " +
75
+ "The modkit README spells the parent 'Subnautica 2' with a space where every observed " +
76
+ "path has none; confirm on a real install before relying on this path.",
77
+ },
78
+ ]);
79
+ },
80
+ async loadOrder(install) {
81
+ const modsDir = path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "ue4ss", "Mods");
82
+ const file = path.join(modsDir, "mods.txt");
83
+ if (!(await pathExists(file)))
84
+ return [];
85
+ const text = await fs.readFile(file, "utf8");
86
+ const entries = [];
87
+ let index = 0;
88
+ for (const raw of text.split(/\r?\n/)) {
89
+ const line = raw.trim();
90
+ if (!line || line.startsWith(";") || line.startsWith("#"))
91
+ continue;
92
+ const m = /^(?<name>[^:]+?)\s*:\s*(?<flag>[01])\s*$/.exec(line);
93
+ if (!m?.groups)
94
+ continue;
95
+ const name = m.groups.name;
96
+ const exists = await pathExists(path.join(modsDir, name));
97
+ entries.push({ index: index++, name, enabled: m.groups.flag === "1", ...(exists ? { modId: `ue4ss:${name}` } : {}) });
98
+ }
99
+ return entries;
100
+ },
101
+ loadOrderFile(install) {
102
+ return path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "ue4ss", "Mods", "mods.txt");
103
+ },
104
+ logs(install) {
105
+ const saved = localAppData("Subnautica2", "Saved", "Logs");
106
+ return logs([
107
+ {
108
+ id: "game",
109
+ label: "Unreal game log",
110
+ candidates: [path.join(saved, "Subnautica2.log"), path.join(saved, "Subnautica2-backup-*.log")],
111
+ notes: "One new file per launch (the previous one becomes Subnautica2-backup-<local stamp>.log). " +
112
+ "The first several hundred lines carry no timestamp prefix, and that is where the pak " +
113
+ "mounts and plugin list are; later lines are [UTC stamp][frame]Category: Verbosity: " +
114
+ "message, with the Verbosity token omitted at the default Log level.",
115
+ },
116
+ {
117
+ id: "ue4ss",
118
+ label: "UE4SS log",
119
+ candidates: [path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "ue4ss", "UE4SS.log"), path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "UE4SS.log")],
120
+ rootId: "ue4ss",
121
+ notes: "Only exists once UE4SS is installed and has run; the experimental layout keeps it under ue4ss/.",
122
+ },
123
+ ]);
124
+ },
125
+ async explainMissing(install) {
126
+ const reasons = {};
127
+ const ue4ssDll = path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "ue4ss", "UE4SS.dll");
128
+ const dwmapi = path.join(install.root, PROJECT_DIR, "Binaries", "Win64", "dwmapi.dll");
129
+ if (!(await pathExists(ue4ssDll)) && !(await pathExists(dwmapi))) {
130
+ reasons.ue4ss = "UE4SS is not installed beside the shipping executable (no ue4ss/UE4SS.dll or dwmapi.dll proxy), so no UE4SS.log exists. Only an experimental UE4SS build supports Unreal 5.6.";
131
+ }
132
+ else {
133
+ reasons.ue4ss = "UE4SS is present but has not written a log since it was installed; launch once.";
134
+ }
135
+ reasons.game = "The game writes Subnautica2.log on every launch under LocalAppData; its absence means the game has not been launched on this account.";
136
+ return reasons;
137
+ },
138
+ logSignatures: SIGNATURES,
139
+ validators: PACKAGING_VALIDATORS,
140
+ compat: subnautica2Compat,
141
+ processNames: ["Subnautica2-Win64-Shipping.exe"],
142
+ guidance: "An IoStore-only Unreal 5.6 game with no official mod support: a legacy .pak alone delivers " +
143
+ "nothing, a pak mod is a .pak/.utoc/.ucas triplet, and every patch can break mods (the " +
144
+ "developer says so). UE4SS must be an experimental build for Unreal 5.6 and is DLL injection " +
145
+ "into a game whose build carries an EOS anti-cheat bootstrapper, inert on a plain launch and " +
146
+ "unresolved for co-op; the loader-free DLC-plugin route under Mods/ avoids both. Only a Steam " +
147
+ "install with the shipping executable present, or MODWRIGHT_SUBNAUTICA2_ROOT, is detected. A " +
148
+ "project declares its folders by source kind: 'paks' (triplets bound for ~mods), 'logicmods' " +
149
+ "(one folder per mod), 'ue4ss' (the Mods folder). " +
150
+ "Nothing here has run against an installed game.",
151
+ };
@@ -0,0 +1,298 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { pathExists } from "../../../core/fsutil.js";
4
+ import { GAME_MOUNT_POINT, hasPrioritySuffix, PakError, readPakInfo, readUtocHeader, TRIPLET_EXTENSIONS, tripletsIn, UtocError } from "../paks.js";
5
+ export const PAKS_SOURCE_KIND = "paks";
6
+ export const LOGICMODS_SOURCE_KIND = "logicmods";
7
+ export const UE4SS_SOURCE_KIND = "ue4ss";
8
+ const TRIPLET_EVIDENCE = ["subnautica2.game.iostore-only-paks", "subnautica2.ue5.iostore-utoc-ucas-split"];
9
+ const NAME_EVIDENCE = ["subnautica2.modding.logicmods-is-folder-per-mod"];
10
+ const MOUNT_EVIDENCE = ["subnautica2.game.pak-mount-point", "subnautica2.ue5.mount-order-and-mount-point", "subnautica2.ue5.pak-footer-and-index-head"];
11
+ const UTOC_EVIDENCE = ["subnautica2.ue5.iostore-utoc-ucas-split", "subnautica2.ue5.utoc-header"];
12
+ const PRIORITY_EVIDENCE = ["subnautica2.ue5.patch-pak-p-suffix", "subnautica2.ue5.tilde-mods-is-a-convention"];
13
+ const UE4SS_EVIDENCE = ["subnautica2.ue4ss.mod-folder-and-mods-txt", "subnautica2.ue4ss.shared-folder-and-enabled-txt"];
14
+ function isWithin(parent, child) {
15
+ const rel = path.relative(parent, child);
16
+ return rel === "" || (!(rel === ".." || rel.startsWith(`..${path.sep}`)) && !path.isAbsolute(rel));
17
+ }
18
+ const containerSources = (project) => project.sources.filter((s) => s.exists && (s.kind === PAKS_SOURCE_KIND || s.kind === LOGICMODS_SOURCE_KIND));
19
+ const appliesToContainers = (target, project) => target.kind === "project" && containerSources(project).length > 0;
20
+ const CONTAINER_APPLIES = `the project, when it has a source of kind "${PAKS_SOURCE_KIND}" or "${LOGICMODS_SOURCE_KIND}"`;
21
+ async function tripletDirs(project) {
22
+ const out = [];
23
+ for (const s of containerSources(project)) {
24
+ if (s.kind === PAKS_SOURCE_KIND) {
25
+ out.push({ sourceId: s.id, kind: s.kind, dir: s.absPath });
26
+ continue;
27
+ }
28
+ const entries = await fs.readdir(s.absPath, { withFileTypes: true });
29
+ for (const e of entries)
30
+ if (e.isDirectory())
31
+ out.push({ sourceId: s.id, kind: s.kind, dir: path.join(s.absPath, e.name), modName: e.name });
32
+ }
33
+ return out;
34
+ }
35
+ const triplet = {
36
+ id: "subnautica2.paks.triplet",
37
+ game: "subnautica2",
38
+ groups: ["packaging"],
39
+ modTypes: ["packaging"],
40
+ summary: "Every packed mod ships all three of .pak, .utoc and .ucas under one stem; a lone legacy .pak mounts and delivers nothing on this IoStore-only game, and a .utoc without its .ucas has no data.",
41
+ appliesTo: appliesToContainers,
42
+ appliesToDescription: CONTAINER_APPLIES,
43
+ needs: { project: true },
44
+ cost: "edit",
45
+ severity: "block",
46
+ evidence: TRIPLET_EVIDENCE,
47
+ async run(ctx) {
48
+ const findings = [];
49
+ for (const d of await tripletDirs(ctx.project)) {
50
+ for (const t of await tripletsIn(d.dir)) {
51
+ const missing = TRIPLET_EXTENSIONS.filter((ext) => !t.present[ext]);
52
+ if (missing.length === 0)
53
+ continue;
54
+ const have = TRIPLET_EXTENSIONS.filter((ext) => t.present[ext]);
55
+ const file = t.present[have[0]];
56
+ const lonePak = have.length === 1 && have[0] === ".pak";
57
+ findings.push({
58
+ validatorId: this.id,
59
+ severity: "block",
60
+ file,
61
+ message: lonePak
62
+ ? `${t.stem}.pak in "${d.sourceId}" has no ${t.stem}.utoc or ${t.stem}.ucas beside it; on this IoStore-only game a legacy pak mounts and delivers nothing.`
63
+ : `${t.stem} in "${d.sourceId}" is missing ${missing.join(" and ")}; the game needs the .pak, .utoc and .ucas together (the .utoc describes the container, the .ucas holds the data).`,
64
+ fix: lonePak ? "Convert the pak with `retoc to-zen` (targeting UE5_6) so the .utoc/.ucas pair is produced, and ship all three." : "Ship the whole triplet the packaging step produced; nothing in it is optional.",
65
+ evidence: TRIPLET_EVIDENCE,
66
+ });
67
+ }
68
+ }
69
+ return findings;
70
+ },
71
+ };
72
+ const nameAgreement = {
73
+ id: "subnautica2.logicmods.name-agreement",
74
+ game: "subnautica2",
75
+ groups: ["packaging"],
76
+ modTypes: ["packaging"],
77
+ summary: "In LogicMods/<Mod>/ the packed files are <Mod>.pak/.utoc/.ucas, named after the folder as the modkit's README requires; a folder with a differently named triplet, or with none, is silently skipped.",
78
+ appliesTo: (target, project) => target.kind === "project" && project.sources.some((s) => s.exists && s.kind === LOGICMODS_SOURCE_KIND),
79
+ appliesToDescription: `the project, when it has a source of kind "${LOGICMODS_SOURCE_KIND}"`,
80
+ needs: { project: true },
81
+ cost: "edit",
82
+ severity: "block",
83
+ evidence: NAME_EVIDENCE,
84
+ async run(ctx) {
85
+ const findings = [];
86
+ for (const s of ctx.project.sources) {
87
+ if (!s.exists || s.kind !== LOGICMODS_SOURCE_KIND)
88
+ continue;
89
+ const entries = await fs.readdir(s.absPath, { withFileTypes: true });
90
+ for (const e of entries) {
91
+ const full = path.join(s.absPath, e.name);
92
+ if (!e.isDirectory()) {
93
+ if (TRIPLET_EXTENSIONS.includes(path.extname(e.name).toLowerCase())) {
94
+ findings.push({ validatorId: this.id, severity: "block", file: full, message: `${e.name} sits directly in LogicMods source "${s.id}"; on this game LogicMods is one folder per mod (LogicMods/<Mod>/<Mod>.pak), not loose paks.`, fix: `Move it into LogicMods/${path.basename(e.name, path.extname(e.name))}/ with its .utoc and .ucas.`, evidence: NAME_EVIDENCE });
95
+ }
96
+ continue;
97
+ }
98
+ const triplets = await tripletsIn(full);
99
+ if (triplets.length === 0) {
100
+ findings.push({ validatorId: this.id, severity: "warn", file: full, message: `LogicMods folder ${e.name}/ in "${s.id}" holds no .pak/.utoc/.ucas; the blueprint mod loader finds nothing to mount.`, evidence: NAME_EVIDENCE });
101
+ continue;
102
+ }
103
+ for (const t of triplets) {
104
+ if (t.stem.toLowerCase() === e.name.toLowerCase())
105
+ continue;
106
+ findings.push({
107
+ validatorId: this.id,
108
+ severity: "block",
109
+ file: Object.values(t.present)[0],
110
+ message: `${t.stem}.* in LogicMods/${e.name}/ does not carry the folder's name; the modkit's README says the packaged files must be named after the mod folder (${e.name}.pak, ${e.name}.utoc, ${e.name}.ucas), and a rename is a silent failure.`,
111
+ fix: `Rename the three files to ${e.name}.pak/.utoc/.ucas, or the folder to ${t.stem}/.`,
112
+ evidence: NAME_EVIDENCE,
113
+ });
114
+ }
115
+ }
116
+ }
117
+ return findings;
118
+ },
119
+ };
120
+ function isContainerFile(ext) {
121
+ return (target, project) => {
122
+ if (target.kind !== "file" || path.extname(target.path).toLowerCase() !== ext)
123
+ return false;
124
+ return containerSources(project).some((s) => isWithin(s.absPath, target.path));
125
+ };
126
+ }
127
+ const mountPoint = {
128
+ id: "subnautica2.paks.mount-point",
129
+ game: "subnautica2",
130
+ groups: ["packaging"],
131
+ modTypes: ["packaging"],
132
+ summary: `A .pak's footer is readable and its index begins with the mount point ${GAME_MOUNT_POINT}, the one the shipped containers use; any other mount point mounts cleanly and resolves nothing.`,
133
+ appliesTo: isContainerFile(".pak"),
134
+ appliesToDescription: `every .pak under a "${PAKS_SOURCE_KIND}" or "${LOGICMODS_SOURCE_KIND}" source`,
135
+ needs: { project: true },
136
+ cost: "edit",
137
+ severity: "block",
138
+ evidence: MOUNT_EVIDENCE,
139
+ async run(ctx) {
140
+ if (ctx.target.kind !== "file")
141
+ return [];
142
+ const file = ctx.target.path;
143
+ const name = path.basename(file);
144
+ try {
145
+ const info = await readPakInfo(file);
146
+ if (info.encryptedIndex) {
147
+ return [{ validatorId: this.id, severity: "warn", file, message: `${name} has an encrypted index (pak version ${info.versionMajor}, ${info.versionName}), so its mount point cannot be read here; the game's own containers are unencrypted and it registers no key for a mod's.`, fix: "Repack without index encryption (repak and retoc both write unencrypted by default).", evidence: MOUNT_EVIDENCE }];
148
+ }
149
+ if (info.mountPoint === GAME_MOUNT_POINT)
150
+ return [];
151
+ return [
152
+ {
153
+ validatorId: this.id,
154
+ severity: "block",
155
+ file,
156
+ message: `${name} mounts at ${JSON.stringify(info.mountPoint)} (pak version ${info.versionMajor}, ${info.versionName}); the game's containers mount at ${JSON.stringify(GAME_MOUNT_POINT)}, and a pak with another mount point mounts without error and delivers files at paths nothing asks for.`,
157
+ fix: `Repack with the mount point ${GAME_MOUNT_POINT} (repak's default; UnrealPak's is not).`,
158
+ evidence: MOUNT_EVIDENCE,
159
+ },
160
+ ];
161
+ }
162
+ catch (err) {
163
+ const reason = err instanceof PakError ? err.reason : "unreadable";
164
+ return [{ validatorId: this.id, severity: "block", file, message: `${name} has no readable pak footer (${reason}): ${err.message}.`, fix: "The file is not a pak or is truncated; repack it.", evidence: MOUNT_EVIDENCE }];
165
+ }
166
+ },
167
+ };
168
+ const utocHeader = {
169
+ id: "subnautica2.utoc.header",
170
+ game: "subnautica2",
171
+ groups: ["packaging"],
172
+ modTypes: ["packaging"],
173
+ summary: "A .utoc starts with the IoStore TOC magic and a known TOC version; a file named .utoc that does not is not a container the game can open.",
174
+ appliesTo: isContainerFile(".utoc"),
175
+ appliesToDescription: `every .utoc under a "${PAKS_SOURCE_KIND}" or "${LOGICMODS_SOURCE_KIND}" source`,
176
+ needs: { project: true },
177
+ cost: "edit",
178
+ severity: "block",
179
+ evidence: UTOC_EVIDENCE,
180
+ async run(ctx) {
181
+ if (ctx.target.kind !== "file")
182
+ return [];
183
+ const file = ctx.target.path;
184
+ const name = path.basename(file);
185
+ try {
186
+ const h = await readUtocHeader(file);
187
+ if (h.version === 0)
188
+ return [{ validatorId: this.id, severity: "block", file, message: `${name} carries TOC version 0 (Invalid).`, evidence: UTOC_EVIDENCE }];
189
+ if (h.entryCount === 0)
190
+ return [{ validatorId: this.id, severity: "warn", file, message: `${name} (TOC version ${h.version}, ${h.versionName}) lists no chunks; the container is empty.`, evidence: UTOC_EVIDENCE }];
191
+ return [];
192
+ }
193
+ catch (err) {
194
+ const reason = err instanceof UtocError ? err.reason : "unreadable";
195
+ return [{ validatorId: this.id, severity: "block", file, message: `${name} has no IoStore TOC header (${reason}): ${err.message}.`, fix: "Produce the .utoc/.ucas pair with `retoc to-zen`; a renamed or truncated file is not a container.", evidence: UTOC_EVIDENCE }];
196
+ }
197
+ },
198
+ };
199
+ const prioritySuffix = {
200
+ id: "subnautica2.paks.priority-suffix",
201
+ game: "subnautica2",
202
+ groups: ["packaging"],
203
+ modTypes: ["packaging"],
204
+ summary: "A _P suffix is Epic's patch-priority marker and is noted; a mod shipped both with and without the suffix has a dead copy; otherwise load order among ~mods paks is alphabetical, later wins.",
205
+ appliesTo: (target, project) => target.kind === "project" && project.sources.some((s) => s.exists && s.kind === PAKS_SOURCE_KIND),
206
+ appliesToDescription: `the project, when it has a source of kind "${PAKS_SOURCE_KIND}"`,
207
+ needs: { project: true },
208
+ cost: "edit",
209
+ severity: "warn",
210
+ evidence: PRIORITY_EVIDENCE,
211
+ async run(ctx) {
212
+ const findings = [];
213
+ for (const s of ctx.project.sources) {
214
+ if (!s.exists || s.kind !== PAKS_SOURCE_KIND)
215
+ continue;
216
+ const triplets = await tripletsIn(s.absPath);
217
+ const stems = new Set(triplets.map((t) => t.stem.toLowerCase()));
218
+ for (const t of triplets) {
219
+ if (!hasPrioritySuffix(t.stem))
220
+ continue;
221
+ const file = Object.values(t.present)[0];
222
+ const bare = t.stem.slice(0, -2);
223
+ if (stems.has(bare.toLowerCase())) {
224
+ findings.push({ validatorId: this.id, severity: "warn", file, message: `${t.stem} and ${bare} are both in "${s.id}"; the _P copy takes priority and the other is a dead duplicate.`, fix: "Ship one of them.", evidence: PRIORITY_EVIDENCE });
225
+ }
226
+ else {
227
+ findings.push({ validatorId: this.id, severity: "info", file, message: `${t.stem} carries the _P suffix, which the engine's mounting system uses to give it priority over other paks regardless of name order.`, evidence: PRIORITY_EVIDENCE });
228
+ }
229
+ }
230
+ }
231
+ return findings;
232
+ },
233
+ };
234
+ const UE4SS_SHARED = "shared";
235
+ const ue4ssModShape = {
236
+ id: "subnautica2.ue4ss.mod-shape",
237
+ game: "subnautica2",
238
+ groups: ["packaging"],
239
+ modTypes: ["packaging", "gameplay-tweak"],
240
+ summary: "Each UE4SS mod folder has scripts/main.lua or dlls/main.dll, and is enabled by a mods.txt line (or an enabled.txt); a mods.txt entry with no folder does nothing.",
241
+ appliesTo: (target, project) => target.kind === "project" && project.sources.some((s) => s.exists && s.kind === UE4SS_SOURCE_KIND),
242
+ appliesToDescription: `the project, when it has a source of kind "${UE4SS_SOURCE_KIND}"`,
243
+ needs: { project: true },
244
+ cost: "edit",
245
+ severity: "warn",
246
+ evidence: UE4SS_EVIDENCE,
247
+ async run(ctx) {
248
+ const findings = [];
249
+ for (const s of ctx.project.sources) {
250
+ if (!s.exists || s.kind !== UE4SS_SOURCE_KIND)
251
+ continue;
252
+ const modsTxt = path.join(s.absPath, "mods.txt");
253
+ const listed = new Map();
254
+ const hasModsTxt = await pathExists(modsTxt);
255
+ if (hasModsTxt) {
256
+ const text = await ctx.readText(modsTxt);
257
+ text.split(/\r?\n/).forEach((raw, i) => {
258
+ const line = raw.trim();
259
+ if (!line || line.startsWith(";") || line.startsWith("#"))
260
+ return;
261
+ const m = /^([^:]+?)\s*:\s*([01])\s*$/.exec(line);
262
+ if (!m) {
263
+ findings.push({ validatorId: this.id, severity: "warn", file: modsTxt, line: i + 1, message: `mods.txt line ${i + 1} is not "ModName : 1" or "ModName : 0": "${line}".`, evidence: UE4SS_EVIDENCE });
264
+ return;
265
+ }
266
+ listed.set(m[1].toLowerCase(), { name: m[1], enabled: m[2] === "1" });
267
+ });
268
+ }
269
+ const entries = await fs.readdir(s.absPath, { withFileTypes: true });
270
+ const folders = entries.filter((e) => e.isDirectory() && e.name.toLowerCase() !== UE4SS_SHARED);
271
+ for (const [key, { name, enabled }] of listed) {
272
+ if (!folders.some((f) => f.name.toLowerCase() === key)) {
273
+ findings.push({ validatorId: this.id, severity: enabled ? "warn" : "info", file: modsTxt, message: `mods.txt ${enabled ? "enables" : "lists"} "${name}", and no folder of that name exists in "${s.id}"; UE4SS loads nothing for it.`, evidence: UE4SS_EVIDENCE });
274
+ }
275
+ }
276
+ for (const f of folders) {
277
+ const dir = path.join(s.absPath, f.name);
278
+ const inner = await fs.readdir(dir, { withFileTypes: true });
279
+ const lower = new Map(inner.map((i) => [i.name.toLowerCase(), i]));
280
+ const hasLua = lower.get("scripts")?.isDirectory() && (await pathExists(path.join(dir, lower.get("scripts").name, "main.lua")));
281
+ const hasDll = lower.get("dlls")?.isDirectory() && (await pathExists(path.join(dir, lower.get("dlls").name, "main.dll")));
282
+ if (!hasLua && !hasDll) {
283
+ findings.push({ validatorId: this.id, severity: "warn", file: dir, message: `UE4SS mod folder ${f.name}/ in "${s.id}" has neither scripts/main.lua nor dlls/main.dll; the loader finds no entry point.`, fix: "A Lua mod needs scripts/main.lua; a C++ mod needs dlls/main.dll.", evidence: UE4SS_EVIDENCE });
284
+ }
285
+ const entry = listed.get(f.name.toLowerCase())?.enabled;
286
+ const enabledTxt = lower.has("enabled.txt");
287
+ if (entry === undefined && !enabledTxt) {
288
+ findings.push({ validatorId: this.id, severity: "warn", file: dir, message: `UE4SS mod folder ${f.name}/ in "${s.id}" is ${hasModsTxt ? "not listed in mods.txt" : "present but there is no mods.txt"} and has no enabled.txt, so UE4SS will not load it.`, fix: `Add "${f.name} : 1" to mods.txt.`, evidence: UE4SS_EVIDENCE });
289
+ }
290
+ else if (entry === false && !enabledTxt) {
291
+ findings.push({ validatorId: this.id, severity: "info", file: dir, message: `UE4SS mod folder ${f.name}/ in "${s.id}" is disabled in mods.txt ("${f.name} : 0").`, evidence: UE4SS_EVIDENCE });
292
+ }
293
+ }
294
+ }
295
+ return findings;
296
+ },
297
+ };
298
+ export const PACKAGING_VALIDATORS = [triplet, nameAgreement, mountPoint, utocHeader, prioritySuffix, ue4ssModShape];