modwright 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bridges/bg3-se/README.md +3 -2
- package/bridges/bg3-se/bridge.json +1 -1
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +115 -1
- package/bridges/cp2077-cet/README.md +3 -2
- package/bridges/cp2077-cet/bridge.json +2 -2
- package/dist/core/bridge/remove.js +48 -0
- package/dist/core/testplan/registry.js +33 -0
- package/dist/server.js +38 -2
- package/knowledge/cyberpunk2077/cet.natives.yaml +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ Epic paths where they apply. Override with `installPath` on any tool, or set
|
|
|
153
153
|
| `deploy` | Copy a built mod into the game's mod roots as one write plan. It refuses an artifact the build did not produce or one older than its sources, prunes what a previous build left behind, and defers a file the running game holds locked. Every overwrite and delete is backed up first. |
|
|
154
154
|
| `rollback` | List the deploy backups under a project's `.modwright/backups/`, or restore one, re-checking every file's hash first. |
|
|
155
155
|
| `reload_plan` | Classify deployed changes into hot / hot-degraded / save-reload / restart / unknown, with the caveat for each and the single most conservative recommendation. |
|
|
156
|
-
| `bridge` | The in-game bridge (BG3 Script Extender, Cyberpunk 2077 CET): `stage` the bridge into a project, check its heartbeat with `status`, `arm` a probe request and `poll` for its result. |
|
|
156
|
+
| `bridge` | The in-game bridge (BG3 Script Extender, Cyberpunk 2077 CET): `stage` the bridge into a project, check its heartbeat with `status`, `arm` a probe request and `poll` for its result, and `remove` it from the game when you are done. |
|
|
157
157
|
| `run_tests` | Run a mod's in-game test plan (`<mod>/tests/*.yaml`) through the bridge. Probe rows are armed and polled; manual rows become a checklist. Results are written back into the plan. |
|
|
158
158
|
| `ledger` | Track claims and decisions across a fix (`<mod>/ledger/`). A claim moves `authored → checker-clean → built → verified \| contradicted`, and nothing is verified without evidence. |
|
|
159
159
|
| `convert` | Run one registered conversion: BG3 lsx↔lsf and xml↔loca through divine, Cyberpunk cr2w↔json through WolvenKit CLI. Each one checks its own round trip. |
|
|
@@ -305,7 +305,7 @@ What it runs, and where it writes:
|
|
|
305
305
|
- **The in-game bridges.** While a verification bridge is deployed and the
|
|
306
306
|
game runs, anything that can write the bridge's request folder can run Lua
|
|
307
307
|
in the game. There is no token. Deploy a bridge only while you verify,
|
|
308
|
-
remove it afterwards, and never ship it (the release validators block a
|
|
308
|
+
remove it afterwards (`bridge action=remove`), and never ship it (the release validators block a
|
|
309
309
|
build that contains it). The bridge READMEs under `bridges/` describe each
|
|
310
310
|
one's trust model.
|
|
311
311
|
|
package/bridges/bg3-se/README.md
CHANGED
|
@@ -93,8 +93,9 @@ from running again; they do not say who wrote it. So:
|
|
|
93
93
|
|
|
94
94
|
- deploy the bridge only to a machine and a profile you control, and only while you are
|
|
95
95
|
verifying a mod;
|
|
96
|
-
- remove
|
|
97
|
-
|
|
96
|
+
- remove it when you are done, before playing normally or sharing the profile:
|
|
97
|
+
`bridge action=remove path=<project> mode=apply` deletes every folder the project's
|
|
98
|
+
`withBridge` targets deployed, with a backup (a dry run first shows which);
|
|
98
99
|
- never ship it. `bg3.history.bridge-never-ships` blocks a release build that contains it.
|
|
99
100
|
|
|
100
101
|
## What this bridge never does
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prune": [
|
|
18
18
|
"ModWrightBridge/**"
|
|
19
19
|
],
|
|
20
|
-
"note": "The deploy target a shipped SE mod uses, verbatim: `bridge stage` adds it to the project's deploy targets when no target already names this rootId, into and variant, so a fresh project deploys the bridge without a hand edit. A deploy
|
|
20
|
+
"note": "The deploy target a shipped SE mod uses, verbatim: `bridge stage` adds it to the project's deploy targets when no target already names this rootId, into and variant, so a fresh project deploys the bridge without a hand edit. A deploy without `--variant withBridge` leaves an installed bridge where it is: a variant's prune runs only in a deploy that writes that variant. Take it out with `bridge action=remove path=<project> mode=apply` when you are done verifying (dry-run first; backed up)."
|
|
21
21
|
},
|
|
22
22
|
"sha256": {
|
|
23
23
|
"algorithm": "sha256",
|
|
@@ -1048,6 +1048,114 @@ Probes.handlers = {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
end,
|
|
1050
1050
|
},
|
|
1051
|
+
-- Workbench probes: session-tier mutations that put the world into a testable state, so
|
|
1052
|
+
-- a common setup is a catalogue row rather than hand-written Lua each time. Listed in
|
|
1053
|
+
-- Probes.MUTATING_KINDS below and gated on the `session` tier like the other mutating rows.
|
|
1054
|
+
|
|
1055
|
+
-- attested 2026-10-04: this row ran live and the entity appeared. The call:
|
|
1056
|
+
-- exEntitySpawner.Spawn('base\characters\entities\gang\gang__wraith_ma.ent',
|
|
1057
|
+
-- Game.GetPlayer():GetWorldTransform(), '')
|
|
1058
|
+
-- returned a userdata handle, and Game.FindEntityByID(handle) resolved the entity.
|
|
1059
|
+
-- Despawn takes the RESOLVED object, not the handle, and the handle does not persist across
|
|
1060
|
+
-- bridge calls, so a despawn belongs in the same cp2077.lua.eval as its spawn.
|
|
1061
|
+
["cp2077.world.spawn"] = {
|
|
1062
|
+
attestation = "attested 2026-10-04 (ran live; the entity appeared)",
|
|
1063
|
+
citation = "exEntitySpawner.Spawn(entPath, GetPlayer():GetWorldTransform(), appearance), a CET-added native (RTTIExtender.cpp)",
|
|
1064
|
+
effect = "mutate",
|
|
1065
|
+
mutates = true,
|
|
1066
|
+
run = function(probe)
|
|
1067
|
+
local p = params(probe)
|
|
1068
|
+
if p.entPath == nil then
|
|
1069
|
+
return errored("params.entPath is required, e.g. \"base\\characters\\entities\\gang\\gang__wraith_ma.ent\"")
|
|
1070
|
+
end
|
|
1071
|
+
local entPath = tostring(p.entPath)
|
|
1072
|
+
local appearance = p.appearance ~= nil and tostring(p.appearance) or ""
|
|
1073
|
+
if not have("Game") then return absent("the `Game` global", "design §8 step 4") end
|
|
1074
|
+
local pl = player()
|
|
1075
|
+
if pl == nil then return failed("Game.GetPlayer() returned nil — load a save first", nil, { entPath = entPath }) end
|
|
1076
|
+
|
|
1077
|
+
local spawner
|
|
1078
|
+
local okG = pcall(function() spawner = exEntitySpawner end)
|
|
1079
|
+
if not okG or spawner == nil then
|
|
1080
|
+
return missingAttested("`exEntitySpawner`", "exEntitySpawner.Spawn", { entPath = entPath })
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
local transform
|
|
1084
|
+
local okT, e = pcall(function() transform = pl:GetWorldTransform() end)
|
|
1085
|
+
if not okT then return errored("GetPlayer():GetWorldTransform() raised: " .. tostring(e), { entPath = entPath }) end
|
|
1086
|
+
|
|
1087
|
+
local handle
|
|
1088
|
+
local okS, e2 = pcall(function() handle = exEntitySpawner.Spawn(entPath, transform, appearance) end)
|
|
1089
|
+
local evidence = {
|
|
1090
|
+
entPath = entPath,
|
|
1091
|
+
appearance = appearance,
|
|
1092
|
+
spawnedAtPlayerTransform = true,
|
|
1093
|
+
note = "spawned at the player's own transform: the player is inside the entity until they separate. " ..
|
|
1094
|
+
"The entity arrives after this call returns, so `resolved` is normally false here. " ..
|
|
1095
|
+
"The id handle does not persist across bridge calls; despawn within one eval (cp2077.lua.eval) or re-find the entity.",
|
|
1096
|
+
}
|
|
1097
|
+
if not okS then return errored("exEntitySpawner.Spawn raised: " .. tostring(e2), evidence) end
|
|
1098
|
+
evidence.handle = handle ~= nil and tostring(handle) or nil
|
|
1099
|
+
if handle == nil then return failed("exEntitySpawner.Spawn returned nil for \"" .. entPath .. "\"", nil, evidence) end
|
|
1100
|
+
local ent
|
|
1101
|
+
pcall(function() ent = Game.FindEntityByID(handle) end)
|
|
1102
|
+
evidence.resolved = ent ~= nil
|
|
1103
|
+
return ok(tostring(handle), evidence)
|
|
1104
|
+
end,
|
|
1105
|
+
},
|
|
1106
|
+
|
|
1107
|
+
-- attested 2026-10-04: a teleport to x+4 moved the player by exactly that.
|
|
1108
|
+
-- Game.GetTeleportationFacility():Teleport(GetPlayer(), Vector4, EulerAngles)
|
|
1109
|
+
["cp2077.player.teleport"] = {
|
|
1110
|
+
attestation = "attested 2026-10-04 (ran live, read back)",
|
|
1111
|
+
citation = "Game.GetTeleportationFacility():Teleport(player, Vector4, EulerAngles), from the CET wiki",
|
|
1112
|
+
effect = "mutate",
|
|
1113
|
+
mutates = true,
|
|
1114
|
+
run = function(probe)
|
|
1115
|
+
local p = params(probe)
|
|
1116
|
+
local x, y, z = tonumber(p.x), tonumber(p.y), tonumber(p.z)
|
|
1117
|
+
if x == nil or y == nil or z == nil then
|
|
1118
|
+
return errored("params.x, params.y and params.z (numbers) are required")
|
|
1119
|
+
end
|
|
1120
|
+
local yaw = tonumber(p.yaw) or 0.0
|
|
1121
|
+
if not have("Game") then return absent("the `Game` global", "design §8 step 4") end
|
|
1122
|
+
local pl = player()
|
|
1123
|
+
if pl == nil then return failed("Game.GetPlayer() returned nil — load a save first") end
|
|
1124
|
+
local evidence = {
|
|
1125
|
+
x = x, y = y, z = z, yaw = yaw,
|
|
1126
|
+
call = "Game.GetTeleportationFacility():Teleport(player, Vector4.new(x,y,z,1), EulerAngles.new(0,0,yaw))",
|
|
1127
|
+
note = "the call and the Vector4/EulerAngles construction from the CET wiki; confirmed live 2026-10-04.",
|
|
1128
|
+
}
|
|
1129
|
+
local okTp, e = pcall(function()
|
|
1130
|
+
Game.GetTeleportationFacility():Teleport(pl, Vector4.new(x, y, z, 1.0), EulerAngles.new(0.0, 0.0, yaw))
|
|
1131
|
+
end)
|
|
1132
|
+
if not okTp then return errored("Teleport raised: " .. tostring(e), evidence) end
|
|
1133
|
+
return ok(true, evidence)
|
|
1134
|
+
end,
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
-- attested 2026-10-04: the level followed 15 -> 16 -> 15, read back each time.
|
|
1138
|
+
-- Game.SetLevel(kind, n, 1) (kind defaults to "Level")
|
|
1139
|
+
["cp2077.player.level"] = {
|
|
1140
|
+
attestation = "attested 2026-10-04 (ran live, read back)",
|
|
1141
|
+
citation = "Game.SetLevel(kind, level, 1), from the CET wiki",
|
|
1142
|
+
effect = "mutate",
|
|
1143
|
+
mutates = true,
|
|
1144
|
+
run = function(probe)
|
|
1145
|
+
local p = params(probe)
|
|
1146
|
+
local lvl = tonumber(p.level)
|
|
1147
|
+
if lvl == nil then return errored("params.level (a number) is required") end
|
|
1148
|
+
local kind = p.kind ~= nil and tostring(p.kind) or "Level"
|
|
1149
|
+
if not have("Game") then return absent("the `Game` global", "design §8 step 4") end
|
|
1150
|
+
local evidence = {
|
|
1151
|
+
level = lvl, kind = kind, call = "Game.SetLevel(kind, level, 1)",
|
|
1152
|
+
note = "from the CET wiki; confirmed live 2026-10-04 (level read back).",
|
|
1153
|
+
}
|
|
1154
|
+
local okL, e = pcall(function() Game.SetLevel(kind, lvl, 1) end)
|
|
1155
|
+
if not okL then return errored("Game.SetLevel raised: " .. tostring(e), evidence) end
|
|
1156
|
+
return ok(true, evidence)
|
|
1157
|
+
end,
|
|
1158
|
+
},
|
|
1051
1159
|
}
|
|
1052
1160
|
-- MODWRIGHT-PROBE-TABLE-END
|
|
1053
1161
|
|
|
@@ -1138,7 +1246,13 @@ end
|
|
|
1138
1246
|
--- The ids allowed to mutate anything: the inventory row it always had, and
|
|
1139
1247
|
--- (since 2026-09-11) the eval row. Named
|
|
1140
1248
|
--- as a constant so the gate below reads as the rule it enforces.
|
|
1141
|
-
Probes.MUTATING_KINDS = {
|
|
1249
|
+
Probes.MUTATING_KINDS = {
|
|
1250
|
+
["cp2077.inventory.add"] = true,
|
|
1251
|
+
["cp2077.lua.eval"] = true,
|
|
1252
|
+
["cp2077.world.spawn"] = true,
|
|
1253
|
+
["cp2077.player.teleport"] = true,
|
|
1254
|
+
["cp2077.player.level"] = true,
|
|
1255
|
+
}
|
|
1142
1256
|
|
|
1143
1257
|
--- Run one probe. NEVER throws: one probe failing can never abort the batch, and a handler
|
|
1144
1258
|
--- that raises is reported as `error` with the raised message.
|
|
@@ -167,8 +167,9 @@ the request grants the `session` tier. There is no token. The nonce and the requ
|
|
|
167
167
|
only stop a consumed or stale request from running again; they do not say who wrote it. So:
|
|
168
168
|
|
|
169
169
|
- deploy the bridge only to an install you control, and only while you are verifying a mod;
|
|
170
|
-
- remove
|
|
171
|
-
|
|
170
|
+
- remove it when you are done, before playing normally:
|
|
171
|
+
`bridge action=remove path=<project> mode=apply` deletes every folder the project's
|
|
172
|
+
`withBridge` targets deployed, with a backup (a dry run first shows which);
|
|
172
173
|
- never ship it. `cp2077.history.bridge-never-ships` blocks a release build that contains it.
|
|
173
174
|
|
|
174
175
|
## What this bridge never does
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"prune": [
|
|
27
27
|
"ModWrightBridge/**"
|
|
28
28
|
],
|
|
29
|
-
"note": "The deploy target `bridge stage` adds to the project's deploy targets when no target already names this rootId, into and variant, so a fresh project deploys the bridge without a hand edit; paste it in yourself only if you want it there before staging. A deploy
|
|
29
|
+
"note": "The deploy target `bridge stage` adds to the project's deploy targets when no target already names this rootId, into and variant, so a fresh project deploys the bridge without a hand edit; paste it in yourself only if you want it there before staging. A deploy without `--variant withBridge` leaves an installed bridge where it is: a variant's prune runs only in a deploy that writes that variant. Take it out with `bridge action=remove path=<project> mode=apply` when you are done verifying (dry-run first; backed up)."
|
|
30
30
|
},
|
|
31
31
|
"sha256": {
|
|
32
32
|
"algorithm": "sha256",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "ModWrightBridge/probes.lua",
|
|
46
|
-
"sha256": "
|
|
46
|
+
"sha256": "759015ac7c1167faecbc5d29e91f567d7c45446164544ad7d72029c115a60df6"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "ModWrightBridge/protocol.lua",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { pathExists } from "../fsutil.js";
|
|
3
|
+
import { escapesRoot } from "../project/outside-root.js";
|
|
4
|
+
import { executePlan, planDelete } from "../safety/index.js";
|
|
5
|
+
export const BRIDGE_VARIANT = "withBridge";
|
|
6
|
+
export async function planRemoveBridge(project, roots) {
|
|
7
|
+
const removals = [];
|
|
8
|
+
const operations = [];
|
|
9
|
+
for (const target of project.project.deploy?.targets ?? []) {
|
|
10
|
+
if (target.variant !== BRIDGE_VARIANT || !target.into)
|
|
11
|
+
continue;
|
|
12
|
+
const base = { rootId: target.rootId, into: target.into };
|
|
13
|
+
if (escapesRoot(target.into)) {
|
|
14
|
+
removals.push({ ...base, action: "outside-root" });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const root = roots.find((r) => r.id === target.rootId);
|
|
18
|
+
if (!root) {
|
|
19
|
+
removals.push({ ...base, action: "unknown-root" });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const dest = path.join(root.path, target.into);
|
|
23
|
+
if (await pathExists(dest)) {
|
|
24
|
+
operations.push(planDelete(dest));
|
|
25
|
+
removals.push({ ...base, path: dest, action: "remove" });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
removals.push({ ...base, path: dest, action: "absent" });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
plan: {
|
|
33
|
+
purpose: `remove the deployed bridge of ${project.project.name}`,
|
|
34
|
+
operations,
|
|
35
|
+
backupRoot: path.join(project.root, ".modwright", "backups"),
|
|
36
|
+
},
|
|
37
|
+
removals,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function removeBridge(project, roots, options = {}) {
|
|
41
|
+
const { plan, removals } = await planRemoveBridge(project, roots);
|
|
42
|
+
const report = await executePlan(plan, {
|
|
43
|
+
mode: options.mode ?? "dry-run",
|
|
44
|
+
...(options.now ? { now: options.now } : {}),
|
|
45
|
+
confidence: { level: "high", assumptions: [], verifiedBy: ["the project's withBridge deploy targets", "the game's mod roots"] },
|
|
46
|
+
});
|
|
47
|
+
return { ...report, removals };
|
|
48
|
+
}
|
|
@@ -355,6 +355,39 @@ const CP2077_PROBES = [
|
|
|
355
355
|
"loot-container template does carry a gameTargetingComponent), and whether the game marks TSF_* isExec. Never " +
|
|
356
356
|
"run in game; stays unverified until a live run exercises it.",
|
|
357
357
|
},
|
|
358
|
+
{
|
|
359
|
+
kind: "cp2077.world.spawn",
|
|
360
|
+
game: "cyberpunk2077",
|
|
361
|
+
context: "cet",
|
|
362
|
+
effect: "mutate",
|
|
363
|
+
status: "attested",
|
|
364
|
+
summary: "Spawn an entity from a resource path at the player's transform (exEntitySpawner.Spawn), returning the entity id handle; Game.FindEntityByID resolves it.",
|
|
365
|
+
citation: "cp2077.world.spawn; exEntitySpawner is a CET-added native (RTTIExtender.cpp); attested 2026-10-04",
|
|
366
|
+
paramsSchema: z.object({ entPath: str(), appearance: z.string().optional() }).strict(),
|
|
367
|
+
evidence: "Ran live 2026-10-04 (request req-muugz1b6-94911e04, game 3.0.80.51928): spawning base\\characters\\entities\\gang\\gang__wraith_ma.ent returned a handle and the NPC appeared where the player stood. Game.FindEntityByID(handle) in the same call returned nothing: the entity arrives after the call returns, so resolved:false is the normal result, not a failure.",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
kind: "cp2077.player.teleport",
|
|
371
|
+
game: "cyberpunk2077",
|
|
372
|
+
context: "cet",
|
|
373
|
+
effect: "mutate",
|
|
374
|
+
status: "attested",
|
|
375
|
+
summary: "Teleport the player to a world position (Game.GetTeleportationFacility():Teleport). Optional yaw.",
|
|
376
|
+
citation: "cp2077.player.teleport (CET wiki); attested 2026-10-04",
|
|
377
|
+
paramsSchema: z.object({ x: z.number(), y: z.number(), z: z.number(), yaw: z.number().optional() }).strict(),
|
|
378
|
+
evidence: "Ran live 2026-10-04 (request req-muugz2is-a30b1095, game 3.0.80.51928): a teleport to x+4 moved the player from -2249.30,2790.43,7.00 to -2245.30,2790.43,7.01, read back through cp2077.lua.eval.",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
kind: "cp2077.player.level",
|
|
382
|
+
game: "cyberpunk2077",
|
|
383
|
+
context: "cet",
|
|
384
|
+
effect: "mutate",
|
|
385
|
+
status: "attested",
|
|
386
|
+
summary: "Set a player progression level (Game.SetLevel(kind, n, 1)); kind defaults to \"Level\".",
|
|
387
|
+
citation: "cp2077.player.level (CET wiki); attested 2026-10-04",
|
|
388
|
+
paramsSchema: z.object({ level: z.number().int(), kind: z.string().optional() }).strict(),
|
|
389
|
+
evidence: "Ran live 2026-10-04 (requests req-muugz2is-a30b1095 and req-muugz457-1ded47cb, game 3.0.80.51928): Game.SetLevel(\"Level\", n, 1) moved the player level 15 to 16 and back to 15, each read back with StatsSystem.GetStatValue(player, gamedataStatType.Level).",
|
|
390
|
+
},
|
|
358
391
|
];
|
|
359
392
|
export const PROBE_CATALOG = [...BG3_PROBES, ...CP2077_PROBES];
|
|
360
393
|
const BY_KIND = new Map(PROBE_CATALOG.map((entry) => [entry.kind, entry]));
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { buildContext, createValidatorRegistry, runValidators } from "./core/val
|
|
|
14
14
|
import { runBuild } from "./core/build/index.js";
|
|
15
15
|
import { classifyReload, detectRedHotTools, runDeploy } from "./core/deploy/index.js";
|
|
16
16
|
import { armBridge, bridgeCandidateDirs, bridgeStatus, cetLogCandidates, listPendingArms, PROTOCOL_VERSION as BRIDGE_PROTOCOL_VERSION, pollCyberpunk, pollForResult, probeSchema as bridgeProbeSchema, readPendingArm, stageBridge } from "./core/bridge/index.js";
|
|
17
|
+
import { removeBridge } from "./core/bridge/remove.js";
|
|
17
18
|
import { runTestPlan } from "./core/testplan/index.js";
|
|
18
19
|
import { applyBuildLedgerHook, applyClaimTransition, applyDecisionRecord, applyValidateLedgerHook, checkLedger, claimsFilePath, listClaims, loadClaims, loadDecisions, saveClaims, saveDecisions, showClaim, } from "./core/ledger/index.js";
|
|
19
20
|
import { IndexStore, defaultCacheDir, findLegacyIndex, indexFile, legacyIndexHint, loadKnowledge, resolveIndexFile, searchFacts, } from "./core/knowledge/index.js";
|
|
@@ -1401,10 +1402,13 @@ server.registerTool("bridge", {
|
|
|
1401
1402
|
"(dry-run by default for both; refuses on a protocolVersion mismatch rather than guessing). 'poll': wait for " +
|
|
1402
1403
|
"that request's result up to a budget of max(15s, 3s x probe count) + window, capped at 300s, " +
|
|
1403
1404
|
"reporting one of four named timeout outcomes rather than a bare 'timed out' — plus, on the CET log transport, " +
|
|
1404
|
-
"'truncated' when the result line was cut mid-write.
|
|
1405
|
+
"'truncated' when the result line was cut mid-write. 'remove': take a deployed bridge back out of the game " +
|
|
1406
|
+
"(every folder the project's withBridge deploy targets name), backed up under the project; dry-run by default. " +
|
|
1407
|
+
"A deploy without the withBridge variant leaves an installed bridge in place. " +
|
|
1408
|
+
"Test-plan execution is run_tests; claims are the " +
|
|
1405
1409
|
"ledger tool.",
|
|
1406
1410
|
inputSchema: {
|
|
1407
|
-
action: z.enum(["status", "arm", "poll", "stage"]),
|
|
1411
|
+
action: z.enum(["status", "arm", "poll", "stage", "remove"]),
|
|
1408
1412
|
game: z
|
|
1409
1413
|
.enum(BRIDGE_GAMES)
|
|
1410
1414
|
.optional()
|
|
@@ -1475,6 +1479,38 @@ server.registerTool("bridge", {
|
|
|
1475
1479
|
return fail(`bridge stage failed: ${error.message}`);
|
|
1476
1480
|
}
|
|
1477
1481
|
}
|
|
1482
|
+
if (action === "remove") {
|
|
1483
|
+
if (!start)
|
|
1484
|
+
return fail('bridge remove requires "path" (the project whose deployed bridge to remove).');
|
|
1485
|
+
const surfaceId = game ?? projectGame;
|
|
1486
|
+
const removeSurface = surfaceId ? getSurface(surfaceId) : undefined;
|
|
1487
|
+
if (!removeSurface)
|
|
1488
|
+
return fail(`Could not determine the game for ${start}; pass game explicitly.`);
|
|
1489
|
+
const install = await resolveInstall(removeSurface, installPath);
|
|
1490
|
+
if (!install)
|
|
1491
|
+
return fail(`No install detected for ${removeSurface.name}.`);
|
|
1492
|
+
try {
|
|
1493
|
+
const resolved = await loadProject(start);
|
|
1494
|
+
const report = await removeBridge(resolved, removeSurface.modRoots(install), { mode });
|
|
1495
|
+
const toRemove = report.removals.filter((r) => r.action === "remove").length;
|
|
1496
|
+
return json({
|
|
1497
|
+
action,
|
|
1498
|
+
game: removeSurface.id,
|
|
1499
|
+
project: resolved.project.name,
|
|
1500
|
+
...report,
|
|
1501
|
+
note: report.removals.length === 0
|
|
1502
|
+
? "The project has no withBridge deploy target: nothing to remove."
|
|
1503
|
+
: toRemove === 0
|
|
1504
|
+
? "No deployed bridge folder was found: nothing to remove."
|
|
1505
|
+
: report.applied
|
|
1506
|
+
? `Removed ${toRemove} bridge folder(s); the backup is under the project's .modwright/backups (rollback restores it).`
|
|
1507
|
+
: `Dry run: ${toRemove} bridge folder(s) would be removed, with a backup. Repeat with mode 'apply'.`,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
catch (error) {
|
|
1511
|
+
return fail(`bridge remove failed: ${error.message}`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1478
1514
|
const gameId = game ?? projectGame;
|
|
1479
1515
|
if (!gameId)
|
|
1480
1516
|
return fail('bridge status/arm/poll requires "game" (or a "path" to a project that names one).');
|
|
@@ -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.
|