modwright 0.1.1 → 0.1.3

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 (65) hide show
  1. package/README.md +123 -15
  2. package/bridges/bg3-se/README.md +15 -0
  3. package/bridges/cp2077-cet/ModWrightBridge/probes.lua +113 -0
  4. package/bridges/cp2077-cet/README.md +17 -1
  5. package/bridges/cp2077-cet/bridge.json +1 -1
  6. package/dist/core/bridge/pending.js +8 -1
  7. package/dist/core/bridge/stage.js +2 -2
  8. package/dist/core/build/run.js +14 -0
  9. package/dist/core/build/stage.js +3 -2
  10. package/dist/core/build/steps/core.js +4 -4
  11. package/dist/core/deploy/plan.js +68 -1
  12. package/dist/core/deploy/reload.js +5 -3
  13. package/dist/core/ini.js +26 -0
  14. package/dist/core/knowledge/facts.js +0 -12
  15. package/dist/core/ledger/hooks.js +22 -5
  16. package/dist/core/logs/registry.js +19 -0
  17. package/dist/core/logs/scan.js +65 -10
  18. package/dist/core/logs/triage.js +79 -2
  19. package/dist/core/managers/mo2.js +121 -0
  20. package/dist/core/managers/vortex.js +52 -0
  21. package/dist/core/ownership/index.js +364 -0
  22. package/dist/core/project/index.js +2 -1
  23. package/dist/core/project/load.js +7 -25
  24. package/dist/core/project/outside-root.js +36 -0
  25. package/dist/core/project/schema.js +38 -1
  26. package/dist/core/safety/backup.js +2 -2
  27. package/dist/core/safety/execute.js +12 -4
  28. package/dist/core/safety/restore.js +62 -0
  29. package/dist/core/testplan/registry.js +51 -2
  30. package/dist/core/text/converters/index.js +1 -1
  31. package/dist/core/text/toolchain.js +2 -2
  32. package/dist/core/toolchain/locate.js +13 -1
  33. package/dist/core/toolchain/run.js +6 -4
  34. package/dist/core/toolchain/types.js +1 -1
  35. package/dist/core/trust.js +105 -0
  36. package/dist/core/writes/index.js +206 -0
  37. package/dist/index.js +33 -9
  38. package/dist/server.js +252 -34
  39. package/dist/surfaces/baldursgate3/surface.js +3 -0
  40. package/dist/surfaces/baldursgate3/validators/story.js +3 -3
  41. package/dist/surfaces/baldursgate3/writes.js +152 -0
  42. package/dist/surfaces/cyberpunk2077/compat.js +8 -4
  43. package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +2 -2
  44. package/dist/surfaces/cyberpunk2077/logs.js +86 -6
  45. package/dist/surfaces/cyberpunk2077/surface.js +13 -1
  46. package/dist/surfaces/cyberpunk2077/validators/cr2w.js +9 -3
  47. package/dist/surfaces/cyberpunk2077/writes.js +67 -0
  48. package/dist/surfaces/eldenring/loaders.js +7 -1
  49. package/dist/surfaces/skyrimse/loadorder.js +2 -26
  50. package/dist/surfaces/skyrimse/surface.js +1 -0
  51. package/dist/surfaces/stardewvalley/logs.js +41 -0
  52. package/dist/surfaces/stardewvalley/surface.js +2 -0
  53. package/dist/surfaces/stardewvalley/writes.js +136 -0
  54. package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +29 -0
  55. package/knowledge/baldursgate3/osiris.tags.yaml +78 -0
  56. package/knowledge/cyberpunk2077/cet.rtti-binding.yaml +227 -0
  57. package/knowledge/cyberpunk2077/cet.sandbox.yaml +22 -0
  58. package/knowledge/cyberpunk2077/codeware.overview.yaml +63 -16
  59. package/knowledge/cyberpunk2077/codeware.systems.yaml +152 -33
  60. package/knowledge/cyberpunk2077/entities.queries.yaml +275 -0
  61. package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +30 -4
  62. package/knowledge/cyberpunk2077/rtti.game-systems.yaml +224 -0
  63. package/package.json +1 -1
  64. package/scaffolds/baldursgate3/new-project.yaml +4 -1
  65. package/scaffolds/cyberpunk2077/new-project.yaml +4 -1
@@ -0,0 +1,136 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { hasTokens, parseContentJson, patchesOf, pick, splitLexically } from "./contentpatcher.js";
4
+ const MAX_INCLUDE_DEPTH = 5;
5
+ const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
6
+ function push(ctx, write) {
7
+ const full = { ...write, file: ctx.file };
8
+ if (ctx.unit)
9
+ full.unit = ctx.unit;
10
+ ctx.writes.push(full);
11
+ }
12
+ function patchWrites(cfg, ctx) {
13
+ const includes = [];
14
+ for (const patch of patchesOf(cfg).patches) {
15
+ const when = obj(pick(patch.raw, "When")?.value);
16
+ const condition = when && Object.keys(when).length > 0 ? JSON.stringify(when) : undefined;
17
+ const action = patch.action?.trim().toLowerCase();
18
+ if (action === "include") {
19
+ for (const from of splitLexically(patch.fromFile ?? "")) {
20
+ includes.push(condition ? { fromFile: from, condition } : { fromFile: from });
21
+ }
22
+ continue;
23
+ }
24
+ const targets = splitLexically(patch.target ?? "");
25
+ for (const target of targets) {
26
+ const certainty = (...parts) => condition || hasTokens(target) || parts.some((p) => hasTokens(p)) ? "conditional" : "definite";
27
+ const base = (extra = {}) => (condition ? { condition, ...extra } : extra);
28
+ if (action === "load") {
29
+ const w = { key: target, keyKind: "asset", op: "load", certainty: certainty(), ...base() };
30
+ if (patch.fromFile)
31
+ w.value = patch.fromFile;
32
+ push(ctx, w);
33
+ }
34
+ else if (action === "editdata") {
35
+ const tf = pick(patch.raw, "TargetField")?.value;
36
+ const prefix = Array.isArray(tf) && tf.length > 0 ? `${target}#${tf.map(String).join(".")}` : undefined;
37
+ const entryKey = (k) => (prefix ? `${prefix}.${k}` : `${target}#${k}`);
38
+ let parts = 0;
39
+ const entries = obj(pick(patch.raw, "Entries")?.value);
40
+ for (const [k, v] of Object.entries(entries ?? {})) {
41
+ push(ctx, { key: entryKey(k), keyKind: "asset-entry", op: v === null ? "remove" : "set", value: v, certainty: certainty(k), ...base() });
42
+ parts++;
43
+ }
44
+ const fields = obj(pick(patch.raw, "Fields")?.value);
45
+ for (const [k, fieldMap] of Object.entries(fields ?? {})) {
46
+ for (const [f, v] of Object.entries(obj(fieldMap) ?? {})) {
47
+ push(ctx, { key: `${entryKey(k)}.${f}`, keyKind: "asset-field", op: v === null ? "remove" : "set", value: v, certainty: certainty(k, f), ...base() });
48
+ parts++;
49
+ }
50
+ }
51
+ const textOps = pick(patch.raw, "TextOperations")?.value;
52
+ for (const op of Array.isArray(textOps) ? textOps : []) {
53
+ const o = obj(op);
54
+ const opTarget = o ? pick(o, "Target")?.value : undefined;
55
+ if (!Array.isArray(opTarget) || opTarget.length < 2)
56
+ continue;
57
+ const [kind, ...rest] = opTarget.map(String);
58
+ const operation = String((o && pick(o, "Operation")?.value) ?? "").toLowerCase();
59
+ const writeOp = operation === "removedelimited" ? "remove" : operation === "replacedelimited" ? "set" : "append";
60
+ const key = kind.toLowerCase() === "fields" ? `${entryKey(rest[0])}.${rest.slice(1).join(".")}` : entryKey(rest.join("."));
61
+ push(ctx, { key, keyKind: kind.toLowerCase() === "fields" ? "asset-field" : "asset-entry", op: writeOp, value: o && pick(o, "Value")?.value, certainty: certainty(...rest), ...base() });
62
+ parts++;
63
+ }
64
+ const moves = pick(patch.raw, "MoveEntries")?.value;
65
+ for (const move of Array.isArray(moves) ? moves : []) {
66
+ const id = obj(move) && pick(obj(move), "ID")?.value;
67
+ if (typeof id !== "string")
68
+ continue;
69
+ push(ctx, { key: entryKey(id), keyKind: "asset-entry", op: "edit", value: move, certainty: certainty(id), ...base() });
70
+ parts++;
71
+ }
72
+ if (parts === 0)
73
+ push(ctx, { key: target, keyKind: "asset", op: "edit", certainty: certainty(), ...base() });
74
+ }
75
+ else if (action === "editimage" || action === "editmap") {
76
+ const w = { key: target, keyKind: "asset", op: "edit", certainty: certainty(), ...base() };
77
+ const toArea = pick(patch.raw, "ToArea")?.value;
78
+ if (toArea !== undefined)
79
+ w.value = { ToArea: toArea };
80
+ push(ctx, w);
81
+ }
82
+ }
83
+ }
84
+ return includes;
85
+ }
86
+ async function readPack(packDir, file, unit, extraction, depth) {
87
+ let cfg;
88
+ try {
89
+ cfg = parseContentJson(await fs.readFile(file, "utf8"));
90
+ }
91
+ catch (error) {
92
+ extraction.errors.push({ file, error: error.message });
93
+ return;
94
+ }
95
+ const ctx = { file, unit, writes: [] };
96
+ const includes = patchWrites(cfg, ctx);
97
+ extraction.writes.push(...ctx.writes);
98
+ for (const include of includes) {
99
+ if (hasTokens(include.fromFile)) {
100
+ extraction.errors.push({ file, error: `Include of "${include.fromFile}" uses tokens; not followed.` });
101
+ continue;
102
+ }
103
+ if (depth >= MAX_INCLUDE_DEPTH) {
104
+ extraction.errors.push({ file, error: `Include of "${include.fromFile}" is nested more than ${MAX_INCLUDE_DEPTH} deep; not followed.` });
105
+ continue;
106
+ }
107
+ const target = path.resolve(packDir, include.fromFile);
108
+ if (!target.startsWith(path.resolve(packDir) + path.sep)) {
109
+ extraction.errors.push({ file, error: `Include of "${include.fromFile}" points outside the pack; not followed.` });
110
+ continue;
111
+ }
112
+ const before = extraction.writes.length;
113
+ await readPack(packDir, target, unit, extraction, depth + 1);
114
+ if (include.condition) {
115
+ for (const w of extraction.writes.slice(before)) {
116
+ w.certainty = "conditional";
117
+ w.condition = w.condition ? `${include.condition} and ${w.condition}` : include.condition;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ export async function extractStardewWrites(mod, root) {
123
+ if (root.id !== "mods")
124
+ return undefined;
125
+ const packs = mod.files.filter((f) => f.toLowerCase() === "content.json" || f.toLowerCase().endsWith("/content.json"));
126
+ if (packs.length === 0)
127
+ return undefined;
128
+ const extraction = { writes: [], errors: [] };
129
+ for (const rel of packs) {
130
+ const packRel = path.posix.dirname(rel);
131
+ const packDir = path.join(mod.path, packRel);
132
+ const unit = packRel === "." ? undefined : packRel;
133
+ await readPack(packDir, path.join(mod.path, rel), unit, extraction, 0);
134
+ }
135
+ return extraction;
136
+ }
@@ -384,3 +384,32 @@ facts:
384
384
  Whether the journal .lsx under Mods/<Mod>/Story/Journals loads as loose .lsx was not separated
385
385
  from the missing compile; the RootTemplates rule (loose .lsx not loaded, .lsf needed) suggests
386
386
  it does not.
387
+ - id: bg3.dialogue.osiris-goals.prefix-convention-is-a-compiler-warning-in-lslib
388
+ claim: In LSLib's Osiris story compiler a database whose name does not start with "DB", a PROC whose
389
+ name does not start with "PROC" or a query whose name does not start with "QRY" produces a
390
+ warning (W26 DbNamingStyle, W23 RuleNamingStyle), never an error, and W23 is switched off by
391
+ default; the story still compiles.
392
+ status: inferred
393
+ source: Norbyte/lslib@6f5f698 LSLib/LS/Story/Compiler/Compiler.cs VerifyIRRule ("Name of PROC ...
394
+ should start with the prefix PROC", "Name of Query ... should start with the prefix QRY"),
395
+ VerifyDatabases ("Name of database ... should start with the prefix DB");
396
+ CompilationContext.cs DiagnosticCode RuleNamingStyle = "W23", DbNamingStyle = "W26",
397
+ CompilationLog() WarningSwitches[RuleNamingStyle] = false
398
+ tags:
399
+ - osiris
400
+ - naming
401
+ - db-prefix
402
+ - proc-prefix
403
+ - qry-prefix
404
+ - compiler
405
+ - warning
406
+ detail: "This is the community compiler's behaviour, not Larian's in-engine one: there the prefix is
407
+ a style check. Nothing read in that source supports the claim, attributed to a Nexus article,
408
+ that a missing prefix silently disables the whole story component; until the game's own
409
+ compiler is shown to behave differently, the bg3.story.osiris-naming-convention validator rule
410
+ stays informational. The checks compare the first 2-4 characters case-insensitively
411
+ (\"PROC\"/\"QRY\"/\"DB\"), and the PROC and QRY checks skip names no longer than the prefix,
412
+ so \"Proc_X\" and \"dbFoo\" pass them."
413
+ related:
414
+ - id: bg3.dialogue.osiris-goals.qry-proc-color-coding
415
+ detail: Agrees on the convention itself; adds what the compiler does when it is broken.
@@ -0,0 +1,78 @@
1
+ game: baldursgate3
2
+ topic: osiris.tags
3
+ title: "Osiris entity tags: SetTag/IsTagged/ClearTag and the tag events, from Larian's modding wiki
4
+ and the generated stubs"
5
+ facts:
6
+ - id: bg3.osiris.tags.settag-istagged-cleartag-signatures
7
+ claim: Osiris tags an entity with the call SetTag((GUIDSTRING)_Target, (TAG)_Tag), tests it with the
8
+ query IsTagged([in](GUIDSTRING)_Target, [in](TAG)_Tag, [out](INTEGER)_Bool), and removes it
9
+ with the call ClearTag((GUIDSTRING)_Source, (TAG)_Tag). In the Script Extender's Osi table
10
+ these are Osi.SetTag(target, tag), Osi.IsTagged(target, tag) -> integer, and
11
+ Osi.ClearTag(target, tag).
12
+ status: verified
13
+ source: wiki pages SetTag ("call SetTag((GUIDSTRING)_Target, (TAG)_Tag)"), IsTagged ("query
14
+ IsTagged([in](GUIDSTRING)_Target, [in](TAG)_Tag, [out](INTEGER)_Bool)"), ClearTag
15
+ ("ClearTag((GUIDSTRING)_Source, (TAG)_Tag)"); LaughingLeader/BG3ModdingTools@ab343b6
16
+ generated/ Osi.lua:4273-4275 (SetTag), :1840-1843 (IsTagged, @return integer bool), :2570-2572
17
+ (ClearTag).
18
+ tags:
19
+ - osiris
20
+ - tag
21
+ - settag
22
+ - istagged
23
+ - cleartag
24
+ - signature
25
+ detail: "Verified as declared signatures only; what the calls do at runtime was not observed. The
26
+ wiki's own examples: SetTag(_ParentA, (TAG)_SHADOW_CURSE_IMMUNE) inside a PROC;
27
+ IsTagged(_Player, (TAG)_DARK_URGE, 1) as an AND condition in a QRY (the trailing 1 binds the
28
+ out-param to \"true\"). A TAG is a UUID-keyed resource, referenced in story scripts by a named
29
+ constant; the wiki examples use (TAG)_NAME constants, which the story compiler resolves."
30
+ - id: bg3.osiris.tags.cleartag-only-clears-osiris-set-tags
31
+ claim: ClearTag removes only tags that Osiris itself set. Tags placed via the editor sidebar on a
32
+ root template or on an object instance cannot be cleared this way (a polymorph status replaces
33
+ them with the target template's, but plain Transform calls leave them), and tags set from
34
+ Anubis or from dialogs can only be cleared from those same systems (dialog tags via
35
+ ClearDialogTag).
36
+ status: community
37
+ source: wiki page ClearTag, "Further Information" section (last edited 2024-12-16); ClearDialogTag
38
+ is Osi.ClearDialogTag(target, tag) at LaughingLeader/BG3ModdingTools@ab343b6
39
+ generated/Osi.lua:2522-2524.
40
+ tags:
41
+ - osiris
42
+ - tag
43
+ - cleartag
44
+ - gotcha
45
+ - roottemplate
46
+ - dialog
47
+ related:
48
+ - bg3.osiris.tags.settag-istagged-cleartag-signatures
49
+ detail: "Practical consequence for a probe or test row: IsTagged answers for every tag source, so a
50
+ tag that \"will not clear\" is usually one that was never Osiris-owned — check the root
51
+ template's sidebar tags before treating a failed ClearTag as a scripting bug."
52
+ - id: bg3.osiris.tags.tag-events-and-inventory-tag-queries
53
+ claim: "Osiris declares tag-related events — CharacterTagEvent(character, tag, event),
54
+ TagCleared(target, tag), TagEvent(tag, event), TagSet(target, tag) — and inventory queries
55
+ that take their tags as a string rather than a TAG: GetByTagInInventory(tags, inventoryHolder)
56
+ -> GUIDSTRING, GetItemByTagInInventory(tags, inventoryHolder) -> ITEM, and
57
+ TaggedItemsGetCountInMagicPockets(tags, source) -> integer. HasAppearanceVisualTag(character,
58
+ tag) -> integer tests visual tags separately."
59
+ status: verified
60
+ source: LaughingLeader/BG3ModdingTools@ab343b6 generated/Osi.Events.lua:237
61
+ CharacterTagEvent(character, tag, event); Osi.Events.lua:1197 TagCleared(target, tag);
62
+ Osi.Events.lua:1201 TagEvent(tag, event); Osi.Events.lua:1205 TagSet(target, tag);
63
+ generated/Osi.lua:796-799 (GetByTagInInventory), :1006-1009 (GetItemByTagInInventory),
64
+ :2136-2139 (TaggedItemsGetCountInMagicPockets), :1347-1350 (HasAppearanceVisualTag).
65
+ tags:
66
+ - osiris
67
+ - tag
68
+ - event
69
+ - inventory
70
+ - tag-expression
71
+ related:
72
+ - bg3.osiris.tags.settag-istagged-cleartag-signatures
73
+ detail: Verified as declared signatures only; when the events fire and what the queries return at
74
+ runtime was not observed. The wiki's search-result snippets describe the string argument as a
75
+ tag expression, but its grammar (how several tags combine) is not established by anything read
76
+ here; test it before relying on more than a single tag name. Recorded so a future BG3 probe
77
+ kind for "is the scaffolded entity tagged" has its call shapes on file; none is registered yet
78
+ (bg3.entity.* probes go through the Script Extender's Ext.Entity, not Osiris tags).
@@ -0,0 +1,227 @@
1
+ game: cyberpunk2077
2
+ topic: cet.rtti-binding
3
+ title: "How Cyber Engine Tweaks binds the game's RTTI to Lua: globals, the Game table, member calls,
4
+ out-params, structs, arrays"
5
+ facts:
6
+ - id: cp2077.cet.rtti-global-functions-reachable-bare-and-via-game-table
7
+ claim: "CET exposes every non-member, non-exec RTTI global function to Lua under three names: the
8
+ bare short name as a sandbox global (`TSF_NPC()`), `Game.TSF_NPC()` (short-name lookup, with
9
+ an overload dispatcher when several arities exist), and `Game[\"TSF_NPC;\"]()` (exact RTTI
10
+ full name). So the TSF_*/TSQ_* target-search constructors listed in the RTTI dump's
11
+ globals.json are callable from a CET console or mod without any wrapper."
12
+ status: inferred
13
+ source: 'src/reverse/RTTIMapper.cpp:186-213 (RegisterDirectGlobals walks apRtti->funcs and sets
14
+ aLuaGlobal[shortName] = RTTIHelper::Get().ResolveFunction(shortName) for every function whose
15
+ full name has no "::" and whose short name has no ";", skipping flags.isExec; the target table
16
+ is the sandbox globals, RTTIMapper.cpp:34, which is the fallback environment of every mod and
17
+ of the console, src/scripting/Sandbox.cpp:6-11, Scripting.cpp:644);
18
+ src/scripting/Scripting.cpp:487 (globals["Game"] = this) and :675-686 (Scripting::Index ->
19
+ RTTIHelper::ResolveFunction); src/reverse/RTTIHelper.cpp:316-343 (branch on ";" in the name:
20
+ full-name GetFunction vs short-name FindFunctions, RTTIHelper.cpp:238-259, with
21
+ MakeInvokableOverload at :444).'
22
+ tags:
23
+ - cet
24
+ - lua
25
+ - rtti
26
+ - globals
27
+ - game-table
28
+ - tsf
29
+ - binding
30
+ related:
31
+ - cp2077.entities.gameobject-getentitiesaroundobject-filter-constructors-exist
32
+ detail: 'One residual the source cannot settle: the bare-global registration is skipped when the
33
+ game marks a function isExec. Nothing in CET says whether TSF_* carry that flag, so the bare
34
+ form is very likely but not proven; the two Game[...] forms do not depend on it. A single
35
+ console line settles all three at once: print(type(TSF_NPC), type(Game.TSF_NPC),
36
+ type(Game["TSF_NPC;"])).'
37
+ - id: cp2077.cet.game-table-resolves-globals-then-scriptgameinstance-statics-and-throws-on-miss
38
+ claim: "`Game.Foo` resolves first against RTTI global functions and then against
39
+ ScriptGameInstance's static functions, including script-added ones (a redscript
40
+ `@addMethod(GameInstance)` registers a global named \"ScriptGameInstance::Foo;...\" which CET
41
+ indexes per class), with the GameInstance parameter supplied automatically. A name that
42
+ resolves nowhere raises a Lua error rather than returning nil, and a successful resolution is
43
+ memoized on the Game table."
44
+ status: inferred
45
+ source: src/reverse/RTTIHelper.cpp:56 (m_pGameInstanceType = GetClass("ScriptGameInstance"));
46
+ :316-343 (globals first, then FindFunctions(m_pGameInstanceType, hash, false)); :59-80
47
+ (ParseGlobalStatics indexes every "Class::Func" global into m_extendedFunctions) and :283-294
48
+ (the static branch searches that map alongside staticFuncs); :598 (GameInstance and out params
49
+ excluded from the arity check) and :657-671 (GameInstance auto-supplied, erroring "can't be
50
+ used yet because game instance is not ready" before the engine is up);
51
+ src/scripting/Scripting.cpp:686 ("Function Foo is not a GameInstance member and is not a
52
+ global.") with s_cThrowLuaErrors = true at RTTIHelper.cpp:17; Scripting.cpp:677-702
53
+ (memoization into m_properties on success only).
54
+ tags:
55
+ - cet
56
+ - lua
57
+ - game-table
58
+ - scriptgameinstance
59
+ - codeware
60
+ - addmethod
61
+ - error-handling
62
+ related:
63
+ - cp2077.rtti.scriptgameinstance-getxxx-system-getters
64
+ - cp2077.entities.gettaggedids-is-a-dynamic-entity-registry-not-a-node-lookup
65
+ detail: "This is why Game.GetDynamicEntitySystem() (Codeware's @addMethod addition, attested in a
66
+ shipped CET mod's script) and Game.GetGameTagSystem() (a native ScriptGameInstance static) go
67
+ through the same door. Probe-design consequence: a bare-global existence check (type(TSF_NPC))
68
+ is nil on a miss, but Game.Missing() throws — every handler in probes.lua that touches Game.*
69
+ must stay pcall-wrapped, which they are. The \"how redscript names an @addMethod(GameInstance)
70
+ function\" half is one reading step beyond CET's code (CET only shows it handles Class::Func
71
+ globals); the attested Game.GetDynamicEntitySystem() call is the evidence that the step holds
72
+ in practice."
73
+ - id: cp2077.cet.member-calls-via-colon-and-out-params-as-extra-returns
74
+ claim: "An RTTI object reaches Lua as a StrongReference/WeakReference/ClassReference userdata whose
75
+ __index resolves properties first, then member functions, so `obj:Method(args)` is the
76
+ supported call form. Out-parameters are never taken from Lua: CET allocates them and appends
77
+ their values after the return value, in declaration order, so a function declared `Bool
78
+ F(CName tag, out array<ref<entEntity>> entities)` is called `local ok, entities = obj:F(tag)`.
79
+ A static function can also be called through an instance."
80
+ status: inferred
81
+ source: "src/scripting/Scripting.cpp:186-200 (usertype __index -> ClassType::Index);
82
+ src/reverse/Type.cpp:229-256 (Index_Impl: properties, then ResolveFunction(class, name,
83
+ hasHandle), cached on the object; :238 a same-named property wins over a method);
84
+ src/reverse/RTTIHelper.cpp:514-566 (ResolveHandle pops the colon-call self as the RED context,
85
+ type-checked against the function's class at :551-553); :673-681 (out params get zeroed
86
+ placeholders) and :742-757 (results = return value, then every isOut param); :598 (out params
87
+ excluded from arity); :386-395 (instance call falls back to static scope)."
88
+ tags:
89
+ - cet
90
+ - lua
91
+ - rtti
92
+ - member-call
93
+ - out-params
94
+ - binding
95
+ related:
96
+ - cp2077.entities.native-gametagsystem-finds-entities-by-tag
97
+ detail: "Direct consequence for the native tag lookup: gameGameTagSystem::GetAllMatchingEntities
98
+ returns two Lua values, `ok, entities`, not the array alone. GetAnyMatchingEntity has no out
99
+ param and returns the handle directly."
100
+ - id: cp2077.cet.native-struct-values-are-classreference-copies-that-round-trip
101
+ claim: A non-handle RTTI class/struct value returned to Lua (for example the gameTargetSearchFilter
102
+ a TSF_* constructor returns) becomes a ClassReference userdata holding its own copy of the
103
+ struct, and passing that userdata back as a struct-typed parameter hands the copy's pointer
104
+ through unchanged. Building such a struct from a Lua table is disabled in CET, so the
105
+ constructor functions are the only way to obtain one.
106
+ status: inferred
107
+ source: "src/reverse/Converter.h:192-247 (ClassConverter: ToLua wraps in ClassReference(type,
108
+ value); ToRED takes aObject.as<ClassReference*>()->GetHandle(), else nullptr; the Lua-table
109
+ path is commented out at :213-227; Is() at :238-247 matches any Class type);
110
+ src/reverse/ClassReference.cpp:9-11 (constructor allocates and Assigns a copy)."
111
+ tags:
112
+ - cet
113
+ - lua
114
+ - rtti
115
+ - struct
116
+ - classreference
117
+ - tsf
118
+ - binding
119
+ related:
120
+ - cp2077.entities.gameobject-getentitiesaroundobject-filter-constructors-exist
121
+ detail: "This removes the \"guessing a native struct layout\" worry recorded earlier for
122
+ GetEntitiesAroundObject(range, searchFilter): the filter is obtained from TSF_NPC()/
123
+ TSF_All(mask)/... and passed straight back. Caveat from the same code: ToRED does not check
124
+ that the ClassReference's type matches the parameter type, so passing the wrong struct is not
125
+ caught by CET. The struct has no listed properties in the RTTI dump, so indexing it from Lua
126
+ yields nothing."
127
+ - id: cp2077.cet.float-params-from-numbers-and-array-returns-as-tables-with-nil-holes
128
+ claim: A Float parameter accepts a plain Lua number (20 and 20.0 alike). An array return value
129
+ becomes a fresh 1-based Lua table; elements that are handles become StrongReference userdata,
130
+ but a null handle inside the array becomes nil, so `#t` and ipairs can stop early — count with
131
+ a pairs loop when the array may contain empty slots.
132
+ status: inferred
133
+ source: src/reverse/LuaRED.h:56-71 (arithmetic types converted from sol::type::number, cdata via
134
+ tostring/tonumber); src/scripting/Scripting.cpp:767-782 (array -> sol::table, result[i+1] =
135
+ ToLua(el)); :748-757 (a null handle returns sol::nil).
136
+ tags:
137
+ - cet
138
+ - lua
139
+ - rtti
140
+ - float
141
+ - array
142
+ - nil-hole
143
+ - binding
144
+ related:
145
+ - cp2077.entities.gameobject-getnpcsaroundobject-exists-in-rtti
146
+ detail: "probes.lua's existing handlers use ipairs over GetTaggedIDs results (EntityID values, not
147
+ handles, so no holes there). The nearby handler iterates handle arrays and should count with
148
+ pairs rather than #; the nil-hole consequence is one reading step from the nil return, hence
149
+ inferred like the rest."
150
+ - id: cp2077.cet.enum-params-accept-number-string-or-enum-but-undeclared-values-become-zero
151
+ claim: "An RTTI enum parameter accepts a Lua number, a member-name string, or an Enum userdata
152
+ (Enum.new(typeName, nameOrValue)), and every RTTI enum is also reachable as a global table
153
+ (TSFMV.Obj_Device). But a number or name that is not a declared member is replaced by 0
154
+ silently: only Enum.new with a misspelled TYPE name raises a Lua error. So an ORed combination
155
+ of members the game did not itself declare (e.g. 31 for every Obj_* bit of
156
+ gametargetingSystemSearchFilterMaskValue) reaches the engine as 0."
157
+ status: inferred
158
+ source: 'src/reverse/Converter.h:67-111 (EnumConverter::ToRED: Enum userdata with type check; nil ->
159
+ 0; sol::type::number -> Enum(type, uint32); string -> Enum(type, name));
160
+ src/reverse/Enum.cpp:42-52 (SetValueSafe assigns only when the value matches a declared entry;
161
+ m_value defaults to 0 at Enum.h:35) and :105-120 (SetValueByName, same for names);
162
+ src/reverse/RTTIHelper.cpp:711-714 (the "parameter N must be X" error only fires on a null
163
+ result, which these paths never produce); src/scripting/Scripting.cpp:219-221 (Enum.new
164
+ constructors) and :223-234 (EnumInt); src/reverse/RTTIMapper.cpp:164-171 and :215-219
165
+ (EnumStatic globals and the TSFMV alias); src/reverse/EnumStatic.cpp:15-33 (any member name
166
+ indexes, typos included). Dump: enums.json lists exactly 20 declared TSFMV values, 31 not
167
+ among them.'
168
+ tags:
169
+ - cet
170
+ - lua
171
+ - rtti
172
+ - enum
173
+ - tsfmv
174
+ - silent-failure
175
+ - binding
176
+ related:
177
+ - cp2077.entities.gameobject-getentitiesaroundobject-filter-constructors-exist
178
+ detail: "Probe-design consequence: never pass a computed mask. Build filters from declared members
179
+ one at a time — TSF_Any(Enum.new(\"gametargetingSystemSearchFilterMaskValue\",
180
+ \"Obj_Device\")) — and combine with TSF_Or/TSF_And (2 to 4 filters; the dump marks tsf3/tsf4
181
+ optional and CET honours optional params, RTTIHelper.cpp:784-788). Use the Enum.new form so a
182
+ wrong type name is a loud error. Which member a sector-placed prop matches (Obj_Device,
183
+ Obj_Other, or none if it has no targeting component) is exactly what a live run must observe;
184
+ print(EnumInt(Enum.new(\"gametargetingSystemSearchFilterMaskValue\", 31))) printing 0 confirms
185
+ the silent-zero path live in one line. Native and script-defined globals register and resolve
186
+ by the same code (RTTIMapper.cpp:188-210; RTTIHelper.cpp:316); CET reads no native/script
187
+ flag. Which is which, per NativeDB's decoder (next fact): TSF_All/Any/Not/ And/Or (plain full
188
+ names) are native statics; TSF_NPC/TSF_EnemyNPC/TSF_Quickhackable (mangled \"Name;\" full
189
+ names) are script-side wrappers — an earlier draft of this fact had that backwards."
190
+ - id: cp2077.cet.nativedb-lua-generator-and-flag-layout-corroborate-the-binding
191
+ claim: "NativeDB, the community viewer built on the same RTTI dump, independently encodes the same
192
+ calling conventions: its \"Copy call\" generator for Lua · CET renders a global function as
193
+ Game.Name(...), a static member as Class.Name(...) with GameInstance aliased to Game, an
194
+ instance member as obj:Name(...), and types a CName parameter as `string | CName`. Its flag
195
+ decoder reads the dump's per-function flag word as bits isPrivate, isProtected, isNative,
196
+ isStatic, isFinal, isThreadSafe, isEvent, isConst, isQuest, isTimer — no exec bit — so the
197
+ dump cannot say whether a function is exec-only."
198
+ status: inferred
199
+ source: "rayshader/cp2077-nativedb at b5d29af: src/shared/formatters/lua.formatter.ts
200
+ (formatStaticCall -> `Game.${func.name}`, formatMemberStaticCall -> `${Class}.${name}` with
201
+ formatAlias mapping GameInstance to Game, formatMemberCall -> `${self}:${name}`,
202
+ LuaPrimitiveDef.CName = 'string | CName'); src/shared/red-ast/red-function.ast.ts:85-105
203
+ (fromJson decodes json.d) and :145-156 (enum RedFunctionFlags). Decoded flag words from
204
+ classes.json/globals.json: TSF_All/Any/Not/And/Or = 12 (isNative, isStatic);
205
+ TSF_NPC/EnemyNPC/Quickhackable = 40 (isStatic, isThreadSafe — script-defined);
206
+ ScriptGameInstance::GetGameTagSystem and GetTargetingSystem = 12 (native static);
207
+ gameGameTagSystem::GetAnyMatchingEntity/GetAllMatchingEntities and
208
+ gametargetingTargetingSystem::GetLookAtObject = 4 (native instance);
209
+ gameObject::GetNPCsAroundObject = 48 (isFinal, isThreadSafe — script-defined);
210
+ entEntity::GetCurrentAppearanceName = 132 (native, const)."
211
+ tags:
212
+ - cet
213
+ - lua
214
+ - rtti
215
+ - nativedb
216
+ - flags
217
+ - corroboration
218
+ related:
219
+ - cp2077.cet.rtti-global-functions-reachable-bare-and-via-game-table
220
+ - cp2077.cet.game-table-resolves-globals-then-scriptgameinstance-statics-and-throws-on-miss
221
+ detail: "Two consequences. First, the `string | CName` typing is a second, independent source for
222
+ passing plain Lua strings where a CName is expected, which probes.lua already relies on
223
+ (GetTaggedIDs(<tag>) with a plain string is attested live). Second, the exec question stays
224
+ open but is now bounded: CET skips bare-global registration only for functions the engine
225
+ flags isExec, that flag is not in the dump's word at all, and exec functions are console cheat
226
+ commands by design, so a TSF_* constructor carrying it would be surprising. The Game.TSF_X
227
+ form is unaffected either way and is what NativeDB itself generates."
@@ -217,3 +217,25 @@ facts:
217
217
  - hook
218
218
  - debugging
219
219
  - bridge
220
+ - id: cp2077.cet.sandbox.exec-func-vs-addmethod-reachability
221
+ claim: "From a CET mod's Lua environment, a module-less redscript `public static exec func Foo(gi:
222
+ GameInstance, ...)` is not reachable as `Game.Foo(...)`: the call returns with no Lua error,
223
+ but the function body never runs. An `@addMethod(<Class>)` instance method is reachable the
224
+ way a vanilla method is: `@addMethod(PlayerPuppet) public func Foo() -> String` is called as
225
+ `Game.GetPlayer():Foo()`, and its return value comes back to Lua. To read a value back from
226
+ redscript, add a method to a live game class and return the value; do not rely on an exec func
227
+ writing a log."
228
+ status: verified
229
+ verified_on: "2.31"
230
+ source: ModWright field verification
231
+ tags:
232
+ - cet
233
+ - redscript
234
+ - addmethod
235
+ - exec-func
236
+ - bridge
237
+ - reachability
238
+ - lua-eval
239
+ detail: "This covers calls from a mod's Lua environment (where a bridge evaluates code). The CET
240
+ console, where a person types `Game.Foo()`, was not retested and may still reach exec funcs.
241
+ RELATED: cp2077.redhottools.hotreload.reloadscripts-recompiles-cleanly-storm-crash-1.3.0."
@@ -6,7 +6,10 @@ facts:
6
6
  claim: "Codeware's own documentation groups its API surface under nine headings: Lifecycle, World,
7
7
  Entities, Player, User Interface, Resources, Localization, Reflection and Utilities."
8
8
  status: community
9
- source: psiberx/cp2077-codeware/README.md, section "Documentation" (list)
9
+ source: 'psiberx/cp2077-codeware/README.md, section "Documentation" (list). Checked against commit
10
+ 3a43182 (2026-08-28): README.md still lists exactly these nine headings in this order; this is
11
+ a documentation table of contents with no corresponding code to confirm or contradict it
12
+ against.'
10
13
  tags:
11
14
  - codeware
12
15
  - overview
@@ -22,8 +25,12 @@ facts:
22
25
  or a specific resource inside any mounted archive is present — the documented uses are
23
26
  verifying that the mod's own archive is actually enabled, and detecting whether another mod
24
27
  (and therefore a resource it ships) is installed."
25
- status: community
26
- source: psiberx/cp2077-codeware/wiki/Home.md, section "Resources > Checking resource existence"
28
+ status: inferred
29
+ source: 'psiberx/cp2077-codeware/wiki/Home.md, section "Resources > Checking resource existence";
30
+ confirmed against commit 3a43182 in scripts/Depot/ResourceDepot.reds: `public native func
31
+ ArchiveExists(name: String) -> Bool` and `public native func ResourceExists(path: ResRef) ->
32
+ Bool` on `public native class ResourceDepot`, reached via `@addMethod(GameInstance) public
33
+ static native func GetResourceDepot() -> ref<ResourceDepot>` in the same file.'
27
34
  tags:
28
35
  - codeware
29
36
  - resourcedepot
@@ -36,8 +43,14 @@ facts:
36
43
  caller registers a callback (`token.RegisterCallback(this, n"OnResourceReady")`) and only
37
44
  accesses `token.GetResource()` inside that callback — resource loading through this API is
38
45
  asynchronous, not a direct return value.
39
- status: community
40
- source: psiberx/cp2077-codeware/wiki/Home.md, section "Resources > Reading resources"
46
+ status: inferred
47
+ source: 'psiberx/cp2077-codeware/wiki/Home.md, section "Resources > Reading resources"; confirmed
48
+ against commit 3a43182 in scripts/Depot/ResourceDepot.reds: `public native func
49
+ LoadResource(path: ResRef) -> ref<ResourceToken>`, and scripts/Depot/ResourceToken.reds:
50
+ `public native class ResourceToken` declares `GetResource() -> ref<CResource>`,
51
+ `RegisterCallback(target: ref<IScriptable>, function: CName)`, plus
52
+ `IsFinished`/`IsLoaded`/`IsFailed` — there is no synchronous accessor, only the
53
+ callback-registration path the wiki describes.'
41
54
  tags:
42
55
  - codeware
43
56
  - resourcedepot
@@ -49,8 +62,14 @@ facts:
49
62
  assigning a resource depot path to it with the `*=` operator (the wiki's example is
50
63
  `weapon.effect *= r"base\gameplay\game_effects\strongmelee.es";`); `*=` is the only
51
64
  initialization form the page shows.
52
- status: community
53
- source: psiberx/cp2077-codeware/wiki/Home.md, section "Resources > Resource references"
65
+ status: inferred
66
+ source: "psiberx/cp2077-codeware/wiki/Home.md, section \"Resources > Resource references\";
67
+ confirmed against commit 3a43182 in scripts/Base/Addons/WeaponObject.reds:
68
+ `@addField(WeaponObject) public native let effect: ResourceRef; // rRef<gameEffectSet>` — the
69
+ exact field the wiki's `weapon.effect *= r\"...\"` example assigns to. The `ResourceRef` type
70
+ itself is Codeware's own wrapper, registered as `RTTI_DEFINE_CLASS(App::ResourceWrapper,
71
+ \"ResourceRef\", {...})` in src/App/Depot/ResourceReference.hpp, whose `LoadPath` method
72
+ resolves and starts loading the assigned path — the backing for `*=`'s effect."
54
73
  tags:
55
74
  - codeware
56
75
  - resourceref
@@ -65,26 +84,48 @@ facts:
65
84
  `DefineTexts()`/`DefineSubtitles()` using `Text`/`TextM`/`TextF` (and
66
85
  `Subtitle`/`SubtitleM`/`SubtitleF`) calls, with gendered variants set either as two separate
67
86
  calls or one three-argument `Text(key, female, male)` call."
68
- status: community
69
- source: psiberx/cp2077-codeware/wiki/Home.md, sections "Localization > Localization providers" and
70
- "Localization > Localization packages"
87
+ status: inferred
88
+ source: 'psiberx/cp2077-codeware/wiki/Home.md, sections "Localization > Localization providers" and
89
+ "Localization > Localization packages"; confirmed against commit 3a43182 in
90
+ scripts/Localization/Module/ModLocalizationProvider.reds (`public abstract class
91
+ ModLocalizationProvider extends ScriptableSystem` with `GetPackage(language: CName) ->
92
+ ref<ModLocalizationPackage>` and `GetFallback() -> CName`) and
93
+ scripts/Localization/Module/ModLocalizationPackage.reds (`public abstract class
94
+ ModLocalizationPackage` with `DefineTexts`/ `DefineSubtitles` and the
95
+ `Text`/`TextF`/`TextM`/`Subtitle`/ `SubtitleF`/`SubtitleM` overloads, including the two-arg
96
+ and three-arg `Text(key, valueF, valueM)` form).'
71
97
  tags:
72
98
  - codeware
73
99
  - localization
74
100
  - modlocalizationprovider
75
101
  - modlocalizationpackage
76
- detail: Texts registered this way are also pushed into the native localization system, so they
102
+ detail: "Texts registered this way are also pushed into the native localization system, so they
77
103
  remain reachable via `GetLocalizedTextByKey`/`GetLocalizedText` and from TweakDB records — not
78
- only from the mod's own redscript.
104
+ only from the mod's own redscript. Confirmed for interface texts specifically:
105
+ src/App/Localization/LocalizationService.cpp hooks the native `LoadTexts` raw function and, in
106
+ `OnLoadTexts`, walks every non-abstract `ModLocalizationProvider` subclass via RTTI, calls its
107
+ `GetOnScreenEntries`, and merges the results into the native
108
+ `localizationPersistenceOnScreenEntries` list by primary/secondary key. Subtitle texts
109
+ (`EntryType.Subtitle`) are not part of that native merge — they stay served through
110
+ `LocalizationSystem.GetSubtitle` at runtime instead."
79
111
  - id: cp2077.codeware.overview.reflection-get-class-and-call
80
112
  claim: "Codeware's `Reflection` API lets a mod resolve a class by name
81
113
  (`Reflection.GetClass(n\"...\")` or `Reflection.GetTypeOf(instance)`), enumerate its
82
114
  properties/functions, and read/write/call them dynamically: `prop.GetValue(instance)`,
83
115
  `getter.Call(instance)`, `setter.Call(instance, [args])`, with a `status`-returning overload
84
116
  of `Call` available to detect argument-count/signature mismatches instead of throwing."
85
- status: community
86
- source: psiberx/cp2077-codeware/wiki/Home.md, sections "Reflection > Inspecting types and values"
87
- and "Reflection > Custom callbacks"
117
+ status: inferred
118
+ source: 'psiberx/cp2077-codeware/wiki/Home.md, sections "Reflection > Inspecting types and values"
119
+ and "Reflection > Custom callbacks"; confirmed against commit 3a43182 in
120
+ scripts/Reflection/Reflection.reds (`public native struct Reflection` with `GetClass(name:
121
+ CName) -> ref<ReflectionClass>` and `GetTypeOf(value: Variant) -> ref<ReflectionType>`),
122
+ scripts/Reflection/ReflectionProp.reds (`GetValue(owner: Variant) -> Variant` /
123
+ `SetValue(owner: Variant, value: Variant)`), and scripts/Reflection/ReflectionFunc.reds
124
+ (`public native class ReflectionMemberFunc extends ReflectionFunc { public native func
125
+ Call(self: ref<IScriptable>, opt args: array<Variant>, opt status: script_ref<Bool>) ->
126
+ Variant }`, mirrored on `ReflectionStaticFunc`). Note: the "status-returning overload" is a
127
+ single `Call` with an optional `script_ref<Bool>` out-parameter, not a second,
128
+ separately-named overload.'
88
129
  tags:
89
130
  - codeware
90
131
  - reflection
@@ -102,7 +143,13 @@ facts:
102
143
  Reflection to check whether a particular class, property or function exists at runtime before
103
144
  touching it, rather than assuming it does.
104
145
  status: community
105
- source: psiberx/cp2077-codeware/wiki/Home.md, section "Reflection > Mods and patches compatibility"
146
+ source: "psiberx/cp2077-codeware/wiki/Home.md, section \"Reflection > Mods and patches
147
+ compatibility\". Checked against commit 3a43182: this is guidance prose (\"In cases where you
148
+ can't use conditional compilation with `ModuleExists()`, you can use reflection...\") with no
149
+ corresponding API of its own — `ModuleExists()` is redscript's own conditional-compilation
150
+ builtin, not something Codeware's source defines, and the Reflection API it recommends is
151
+ confirmed separately by cp2077.codeware.overview.reflection-get-class-and-call. Left community
152
+ rather than inferred since no code confirms or contradicts this specific piece of advice."
106
153
  tags:
107
154
  - codeware
108
155
  - moduleexists