modwright 0.1.3 → 0.1.5

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.
@@ -3,7 +3,7 @@ import { readPeFileVersion } from "../../../core/compat/pe.js";
3
3
  import { compareVersions, parseVersion } from "../../../core/compat/version.js";
4
4
  import { FORM_VERSION_LE, FORM_VERSION_SSE, HEADER_VERSION_EXPANDED_LIGHT_RANGE, MASTER_LIMIT_SSE, PluginHeaderError, VALID_HEADER_VERSIONS_SSE, pluginExtension, readPluginHeader, } from "../plugins.js";
5
5
  import { RUNTIME_EXPANDED_LIGHT_RANGE } from "../compat.js";
6
- const EVIDENCE = ["ModWright research"];
6
+ const HEADER_EVIDENCE = ["sse.plugins.format.record-header-is-24-bytes", "sse.plugins.format.hedr-is-twelve-bytes"];
7
7
  function isPluginUnderSource(target, project) {
8
8
  if (target.kind !== "file" || pluginExtension(target.path) === undefined)
9
9
  return false;
@@ -26,7 +26,7 @@ async function headerOrFinding(id, file) {
26
26
  file,
27
27
  message: `${path.basename(file)} has no readable TES4 header (${reason}): ${err.message}`,
28
28
  fix: "A plugin must begin with a TES4 record; re-save it from the Creation Kit or xEdit, or check the file is not truncated.",
29
- evidence: EVIDENCE,
29
+ evidence: HEADER_EVIDENCE,
30
30
  };
31
31
  }
32
32
  }
@@ -42,6 +42,7 @@ function findEntry(order, name) {
42
42
  const key = name.toLowerCase();
43
43
  return order.find((e) => e.name.toLowerCase() === key);
44
44
  }
45
+ const MASTERSPRESENT_EVIDENCE = ["sse.plugins.format.mast-data-pairs-are-the-master-list", "sse.loadorder.files.five-masters-are-implicit-and-absent"];
45
46
  const mastersPresent = {
46
47
  id: "sse.plugins.masters-present",
47
48
  game: "skyrimse",
@@ -53,7 +54,7 @@ const mastersPresent = {
53
54
  needs: { project: true, install: true },
54
55
  cost: "edit",
55
56
  severity: "block",
56
- evidence: EVIDENCE,
57
+ evidence: MASTERSPRESENT_EVIDENCE,
57
58
  async run(ctx) {
58
59
  if (ctx.target.kind !== "file")
59
60
  return [];
@@ -79,12 +80,13 @@ const mastersPresent = {
79
80
  ? `Master "${master}" is under Data but not in the load order at all; ${path.basename(ctx.target.path)} depends on it.`
80
81
  : `Master "${master}" is neither in the load order nor under Data; ${path.basename(ctx.target.path)} cannot load without it.`,
81
82
  fix: entry ? `Activate ${master} (a * line in Plugins.txt).` : `Install ${master} and activate it ahead of ${path.basename(ctx.target.path)}.`,
82
- evidence: EVIDENCE,
83
+ evidence: MASTERSPRESENT_EVIDENCE,
83
84
  });
84
85
  }
85
86
  return findings;
86
87
  },
87
88
  };
89
+ const MASTERORDER_EVIDENCE = ["sse.plugins.format.mast-data-pairs-are-the-master-list", "sse.loadorder.files.master-hoisting"];
88
90
  const masterOrder = {
89
91
  id: "sse.plugins.master-order",
90
92
  game: "skyrimse",
@@ -96,7 +98,7 @@ const masterOrder = {
96
98
  needs: { project: true, install: true },
97
99
  cost: "edit",
98
100
  severity: "block",
99
- evidence: [...EVIDENCE],
101
+ evidence: [...MASTERORDER_EVIDENCE],
100
102
  async run(ctx) {
101
103
  if (ctx.target.kind !== "file")
102
104
  return [];
@@ -114,7 +116,7 @@ const masterOrder = {
114
116
  severity: "info",
115
117
  file: ctx.target.path,
116
118
  message: `${path.basename(ctx.target.path)} is not in the load order, so its masters' positions relative to it cannot be checked yet.`,
117
- evidence: EVIDENCE,
119
+ evidence: MASTERORDER_EVIDENCE,
118
120
  },
119
121
  ];
120
122
  }
@@ -129,12 +131,13 @@ const masterOrder = {
129
131
  file: ctx.target.path,
130
132
  message: `Master "${master}" loads at position ${entry.index}, after ${path.basename(ctx.target.path)} at ${self.index}; a plugin must load after every master it depends on.`,
131
133
  fix: `Move ${master} above ${path.basename(ctx.target.path)} in Plugins.txt (LOOT does this).`,
132
- evidence: [...EVIDENCE],
134
+ evidence: [...MASTERORDER_EVIDENCE],
133
135
  });
134
136
  }
135
137
  return findings;
136
138
  },
137
139
  };
140
+ const ESLHEADERVERSION_EVIDENCE = ["sse.plugins.esl.hedr-1-71-unlocks-the-expanded-range", "sse.plugins.esl.masters-at-1-71-force-the-plugin-to-1-71", "sse.plugins.esl.1-71-plugins-crash-old-game-builds"];
138
141
  const eslHeaderVersion = {
139
142
  id: "sse.plugins.esl-header-version",
140
143
  game: "skyrimse",
@@ -146,7 +149,7 @@ const eslHeaderVersion = {
146
149
  needs: { project: true },
147
150
  cost: "edit",
148
151
  severity: "block",
149
- evidence: EVIDENCE,
152
+ evidence: ESLHEADERVERSION_EVIDENCE,
150
153
  async run(ctx) {
151
154
  if (ctx.target.kind !== "file")
152
155
  return [];
@@ -161,7 +164,7 @@ const eslHeaderVersion = {
161
164
  severity: "block",
162
165
  file: ctx.target.path,
163
166
  message: `${path.basename(ctx.target.path)} has no HEDR subrecord; every plugin's TES4 record carries one.`,
164
- evidence: EVIDENCE,
167
+ evidence: ESLHEADERVERSION_EVIDENCE,
165
168
  });
166
169
  return findings;
167
170
  }
@@ -172,7 +175,7 @@ const eslHeaderVersion = {
172
175
  file: ctx.target.path,
173
176
  message: `HEDR version ${version.toFixed(2)} is not one Skyrim SE accepts (${VALID_HEADER_VERSIONS_SSE.map((v) => v.toFixed(2)).join(", ")}).`,
174
177
  fix: "Re-save the plugin with the Skyrim SE Creation Kit or xEdit in SSE mode.",
175
- evidence: EVIDENCE,
178
+ evidence: ESLHEADERVERSION_EVIDENCE,
176
179
  });
177
180
  return findings;
178
181
  }
@@ -185,7 +188,7 @@ const eslHeaderVersion = {
185
188
  file: ctx.target.path,
186
189
  message: `${path.basename(ctx.target.path)} has HEDR version ${version.toFixed(2)}, which needs game runtime ${RUNTIME_EXPANDED_LIGHT_RANGE} or later; this install is ${runtime}.`,
187
190
  fix: "Update the game, or re-save the plugin with a 1.70 header if it uses no FormID below 0x800.",
188
- evidence: EVIDENCE,
191
+ evidence: ESLHEADERVERSION_EVIDENCE,
189
192
  });
190
193
  }
191
194
  }
@@ -204,7 +207,7 @@ const eslHeaderVersion = {
204
207
  file: ctx.target.path,
205
208
  message: `Master "${master}" is a ${mh.headerVersion.toFixed(2)} plugin while ${path.basename(ctx.target.path)} is ${version.toFixed(2)}; referencing or overriding that master's records in the extended FormID range (001-7FF) will error.`,
206
209
  fix: "Re-save this plugin with a 1.71 header (xEdit 4.1.5b or later, or the current Creation Kit).",
207
- evidence: EVIDENCE,
210
+ evidence: ESLHEADERVERSION_EVIDENCE,
208
211
  });
209
212
  }
210
213
  }
@@ -217,6 +220,7 @@ const eslHeaderVersion = {
217
220
  return findings;
218
221
  },
219
222
  };
223
+ const FLAGEXTENSIONMISMATCH_EVIDENCE = ["sse.plugins.esl.extension-does-not-determine-lightness", "sse.plugins.format.tes4-flag-bits"];
220
224
  const flagExtensionMismatch = {
221
225
  id: "sse.plugins.flag-extension-mismatch",
222
226
  game: "skyrimse",
@@ -228,7 +232,7 @@ const flagExtensionMismatch = {
228
232
  needs: { project: true },
229
233
  cost: "edit",
230
234
  severity: "warn",
231
- evidence: EVIDENCE,
235
+ evidence: FLAGEXTENSIONMISMATCH_EVIDENCE,
232
236
  async run(ctx) {
233
237
  if (ctx.target.kind !== "file")
234
238
  return [];
@@ -245,7 +249,7 @@ const flagExtensionMismatch = {
245
249
  file: ctx.target.path,
246
250
  message: `${name} has the ESM flag set: the game treats it as a master regardless of the .esp extension, so it loads among the masters and every header-reading tool disagrees with its name.`,
247
251
  fix: "Clear the ESM flag in xEdit, or rename the file .esm so the extension says what the flag does.",
248
- evidence: EVIDENCE,
252
+ evidence: FLAGEXTENSIONMISMATCH_EVIDENCE,
249
253
  },
250
254
  ];
251
255
  }
@@ -257,13 +261,14 @@ const flagExtensionMismatch = {
257
261
  file: ctx.target.path,
258
262
  message: `${name} has no ESM flag: the .esm extension makes the game load it as a master anyway, but tools reading the header will not.`,
259
263
  fix: "Set the ESM flag in xEdit so the header and the extension agree.",
260
- evidence: EVIDENCE,
264
+ evidence: FLAGEXTENSIONMISMATCH_EVIDENCE,
261
265
  },
262
266
  ];
263
267
  }
264
268
  return [];
265
269
  },
266
270
  };
271
+ const ESLEXTENSIONIMPLIESFLAG_EVIDENCE = ["sse.plugins.esl.flag-is-bit-9-of-the-tes4-header", "sse.plugins.esl.extension-does-not-determine-lightness"];
267
272
  const eslExtensionImpliesFlag = {
268
273
  id: "sse.plugins.esl-extension-implies-flag",
269
274
  game: "skyrimse",
@@ -275,7 +280,7 @@ const eslExtensionImpliesFlag = {
275
280
  needs: { project: true },
276
281
  cost: "edit",
277
282
  severity: "warn",
278
- evidence: EVIDENCE,
283
+ evidence: ESLEXTENSIONIMPLIESFLAG_EVIDENCE,
279
284
  async run(ctx) {
280
285
  if (ctx.target.kind !== "file")
281
286
  return [];
@@ -291,11 +296,12 @@ const eslExtensionImpliesFlag = {
291
296
  file: ctx.target.path,
292
297
  message: `${path.basename(ctx.target.path)} is an .esl without the light flag (0x200) in its header; the game treats it as light because of the extension, header-reading tools do not.`,
293
298
  fix: "Set the ESL flag in xEdit (it checks the plugin's new records fit the light range first).",
294
- evidence: EVIDENCE,
299
+ evidence: ESLEXTENSIONIMPLIESFLAG_EVIDENCE,
295
300
  },
296
301
  ];
297
302
  },
298
303
  };
304
+ const FORMVERSION_EVIDENCE = ["sse.plugins.format.form-version-44-for-sse"];
299
305
  const formVersion = {
300
306
  id: "sse.plugins.form-version",
301
307
  game: "skyrimse",
@@ -307,7 +313,7 @@ const formVersion = {
307
313
  needs: { project: true },
308
314
  cost: "edit",
309
315
  severity: "block",
310
- evidence: EVIDENCE,
316
+ evidence: FORMVERSION_EVIDENCE,
311
317
  async run(ctx) {
312
318
  if (ctx.target.kind !== "file")
313
319
  return [];
@@ -326,11 +332,12 @@ const formVersion = {
326
332
  ? `${path.basename(ctx.target.path)} has form version 43: a Skyrim Legendary Edition plugin, not converted for Special Edition.`
327
333
  : `${path.basename(ctx.target.path)} has form version ${header.formVersion}; Skyrim SE plugins carry 44.`,
328
334
  fix: le ? "Open and re-save the plugin in the Skyrim SE Creation Kit to convert it (and re-pack any LE .bsa)." : "Re-save the plugin with the Skyrim SE Creation Kit or xEdit.",
329
- evidence: EVIDENCE,
335
+ evidence: FORMVERSION_EVIDENCE,
330
336
  },
331
337
  ];
332
338
  },
333
339
  };
340
+ const MASTERCOUNT_EVIDENCE = ["sse.plugins.esl.master-list-limit-is-253"];
334
341
  const masterCount = {
335
342
  id: "sse.plugins.master-count",
336
343
  game: "skyrimse",
@@ -342,7 +349,7 @@ const masterCount = {
342
349
  needs: { project: true },
343
350
  cost: "edit",
344
351
  severity: "block",
345
- evidence: EVIDENCE,
352
+ evidence: MASTERCOUNT_EVIDENCE,
346
353
  async run(ctx) {
347
354
  if (ctx.target.kind !== "file")
348
355
  return [];
@@ -358,7 +365,7 @@ const masterCount = {
358
365
  file: ctx.target.path,
359
366
  message: `${path.basename(ctx.target.path)} names ${header.masters.length} masters; Skyrim SE allows ${MASTER_LIMIT_SSE}.`,
360
367
  fix: "Clean unused masters in xEdit, or split the plugin.",
361
- evidence: EVIDENCE,
368
+ evidence: MASTERCOUNT_EVIDENCE,
362
369
  },
363
370
  ];
364
371
  },
@@ -0,0 +1,48 @@
1
+ game: cyberpunk2077
2
+ topic: cet.natives
3
+ title: CET-added native functions used to set up a test (spawn, teleport, level)
4
+ facts:
5
+ - id: cp2077.cet.natives.exentityspawner-spawn-despawn
6
+ claim: "`exEntitySpawner` is a native that CET adds. `exEntitySpawner.Spawn(entPath, transform,
7
+ appearance)` spawns an entity from a resource path (backslash form, e.g.
8
+ `base\\characters\\entities\\gang\\gang__wraith_ma.ent`) at the given WorldTransform and
9
+ returns an entity id handle (userdata); `Game.FindEntityByID(handle)` resolves it to the
10
+ entity object. `exEntitySpawner.Despawn(entity)` despawns it, and takes the resolved object,
11
+ not the id handle."
12
+ status: verified
13
+ verified_on: "2.31"
14
+ source: ModWright field verification
15
+ tags:
16
+ - cet
17
+ - native
18
+ - spawn
19
+ - despawn
20
+ - workbench
21
+ - exentityspawner
22
+ detail: "Backs the probe cp2077.world.spawn. Two caveats from the 2026-09-12 run: spawning at the
23
+ player's own transform puts the player inside the spawned entity until they separate; and the
24
+ id handle does not survive across bridge calls (each cp2077.lua.eval is a fresh state), so a
25
+ despawn or respawn has to resolve the entity within the same call. No call is known that turns
26
+ a stored id string back into a live EntityID. CET's WorldFunctionalTests
27
+ (SpawnEntity/DespawnEntity) is the other spawn native it adds; this fact is about
28
+ exEntitySpawner, the one exercised. On 2026-10-04 the spawned entity appeared, but
29
+ `Game.FindEntityByID(handle)` in the same call returned nothing: the entity arrives after
30
+ `Spawn` returns, so resolve it in a later call."
31
+ - id: cp2077.cet.natives.teleport-and-setlevel-work-from-cet
32
+ claim: From CET Lua, `Game.GetTeleportationFacility():Teleport(Game.GetPlayer(), Vector4.new(x, y,
33
+ z, 1), EulerAngles.new(0, 0, yaw))` moves the player to that position, and
34
+ `Game.SetLevel("Level", n, 1)` sets the player level; both take effect at once. The level
35
+ reads back as `Game.GetStatsSystem():GetStatValue(Game.GetPlayer():GetEntityID(),
36
+ gamedataStatType.Level)`.
37
+ status: verified
38
+ verified_on: "2.31"
39
+ source: ModWright field verification
40
+ tags:
41
+ - cet
42
+ - native
43
+ - teleport
44
+ - level
45
+ - workbench
46
+ detail: Backs the probes cp2077.player.teleport and cp2077.player.level. The teleport's z settled
47
+ 0.01 above the target, which is the ground under the player. A save that has been teleported
48
+ or levelled this way should not be kept.
@@ -60,7 +60,7 @@ facts:
60
60
  - id: cp2077.tweakxl.yaml-syntax.duplicate-keys-break-file
61
61
  claim: A duplicate key at the same nesting level in a TweakXL yaml file breaks the whole file's
62
62
  parse; the community-recommended check is to run the file through yamllint.com before packing.
63
- status: community
63
+ status: contradicted
64
64
  source: CDPR-Modding-Documentation/Cyberpunk-Modding-Docs
65
65
  for-mod-creators-theory/core-mods-explained/tweakxl/tweakxl-changing-game-records/how-to-yaml-tweak-modding-basics.md,
66
66
  section "Key Uniqueness"
@@ -69,6 +69,26 @@ facts:
69
69
  - yaml
70
70
  - duplicate-key
71
71
  - validator
72
+ detail: "Contradicted for TweakXL 1.11.4: on 2026-10-04 it read seven installed files that repeat a
73
+ top-level key and logged no error for any of them, while it does log `[error] yaml-cpp: ...`
74
+ for a file that fails to parse
75
+ (cp2077.tweakxl.hotreload.parse-error-still-ends-in-import-completed). See
76
+ cp2077.tweakxl.yaml-syntax.duplicate-keys-do-not-fail-the-parse."
77
+ - id: cp2077.tweakxl.yaml-syntax.duplicate-keys-do-not-fail-the-parse
78
+ claim: "TweakXL 1.11.4 reads a yaml file that repeats a top-level key without a parse error: its log
79
+ shows the file being read and no error line, and the rest of the import goes on. Which
80
+ occurrence wins, or whether both apply, is not established."
81
+ status: verified
82
+ verified_on: "2.31"
83
+ source: ModWright field verification
84
+ supersedes: cp2077.tweakxl.yaml-syntax.duplicate-keys-break-file
85
+ tags:
86
+ - tweakxl
87
+ - yaml
88
+ - duplicate-key
89
+ detail: ModWright's write census (who_touches) reads such a file with every occurrence, in file
90
+ order. The yaml-lint rule still reports a repeated key in a project's own file, where it is
91
+ usually a mistake.
72
92
  - id: cp2077.tweakxl.yaml-syntax.record-names-must-be-unique
73
93
  claim: Every TweakXL record (a named container grouping properties, which may themselves be other
74
94
  records or flats) must have a globally unique name; two records sharing a name overwrite each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modwright",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A game-agnostic MCP server for game modding, with per-game surfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",