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.
- package/README.md +123 -15
- package/bridges/bg3-se/README.md +15 -0
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +113 -0
- package/bridges/cp2077-cet/README.md +17 -1
- package/bridges/cp2077-cet/bridge.json +1 -1
- package/dist/core/bridge/pending.js +8 -1
- package/dist/core/bridge/stage.js +2 -2
- package/dist/core/build/run.js +14 -0
- package/dist/core/build/stage.js +3 -2
- package/dist/core/build/steps/core.js +4 -4
- package/dist/core/deploy/plan.js +68 -1
- package/dist/core/deploy/reload.js +5 -3
- package/dist/core/ini.js +26 -0
- package/dist/core/knowledge/facts.js +0 -12
- package/dist/core/ledger/hooks.js +22 -5
- package/dist/core/logs/registry.js +19 -0
- package/dist/core/logs/scan.js +65 -10
- package/dist/core/logs/triage.js +79 -2
- package/dist/core/managers/mo2.js +121 -0
- package/dist/core/managers/vortex.js +52 -0
- package/dist/core/ownership/index.js +364 -0
- package/dist/core/project/index.js +2 -1
- package/dist/core/project/load.js +7 -25
- package/dist/core/project/outside-root.js +36 -0
- package/dist/core/project/schema.js +38 -1
- package/dist/core/safety/backup.js +2 -2
- package/dist/core/safety/execute.js +12 -4
- package/dist/core/safety/restore.js +62 -0
- package/dist/core/testplan/registry.js +51 -2
- package/dist/core/text/converters/index.js +1 -1
- package/dist/core/text/toolchain.js +2 -2
- package/dist/core/toolchain/locate.js +13 -1
- package/dist/core/toolchain/run.js +6 -4
- package/dist/core/toolchain/types.js +1 -1
- package/dist/core/trust.js +105 -0
- package/dist/core/writes/index.js +206 -0
- package/dist/index.js +33 -9
- package/dist/server.js +252 -34
- package/dist/surfaces/baldursgate3/surface.js +3 -0
- package/dist/surfaces/baldursgate3/validators/story.js +3 -3
- package/dist/surfaces/baldursgate3/writes.js +152 -0
- package/dist/surfaces/cyberpunk2077/compat.js +8 -4
- package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +2 -2
- package/dist/surfaces/cyberpunk2077/logs.js +86 -6
- package/dist/surfaces/cyberpunk2077/surface.js +13 -1
- package/dist/surfaces/cyberpunk2077/validators/cr2w.js +9 -3
- package/dist/surfaces/cyberpunk2077/writes.js +67 -0
- package/dist/surfaces/eldenring/loaders.js +7 -1
- package/dist/surfaces/skyrimse/loadorder.js +2 -26
- package/dist/surfaces/skyrimse/surface.js +1 -0
- package/dist/surfaces/stardewvalley/logs.js +41 -0
- package/dist/surfaces/stardewvalley/surface.js +2 -0
- package/dist/surfaces/stardewvalley/writes.js +136 -0
- package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +29 -0
- package/knowledge/baldursgate3/osiris.tags.yaml +78 -0
- package/knowledge/cyberpunk2077/cet.rtti-binding.yaml +227 -0
- package/knowledge/cyberpunk2077/cet.sandbox.yaml +22 -0
- package/knowledge/cyberpunk2077/codeware.overview.yaml +63 -16
- package/knowledge/cyberpunk2077/codeware.systems.yaml +152 -33
- package/knowledge/cyberpunk2077/entities.queries.yaml +275 -0
- package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +30 -4
- package/knowledge/cyberpunk2077/rtti.game-systems.yaml +224 -0
- package/package.json +1 -1
- package/scaffolds/baldursgate3/new-project.yaml +4 -1
- package/scaffolds/cyberpunk2077/new-project.yaml +4 -1
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ The core is generic. Each game is a **surface**: one module that declares
|
|
|
15
15
|
where things live and what the rules are, so every tool works for every
|
|
16
16
|
supported game.
|
|
17
17
|
|
|
18
|
-
**Status:** 0.x
|
|
19
|
-
|
|
18
|
+
**Status:** 0.x, and the API is not stable yet. How far each game is tested
|
|
19
|
+
differs a lot; "Supported games" says which surfaces have been used against
|
|
20
|
+
a real install and which are built from documentation alone.
|
|
20
21
|
|
|
21
22
|
## Why
|
|
22
23
|
|
|
@@ -63,18 +64,65 @@ without tcli." Most features need nothing. ModWright can install some
|
|
|
63
64
|
external tools itself, and any other tool or data folder only has to be
|
|
64
65
|
pointed at once.
|
|
65
66
|
|
|
67
|
+
## Which mod is doing this?
|
|
68
|
+
|
|
69
|
+
With hundreds of mods installed, the usual question is not "is something
|
|
70
|
+
broken" but "which mod did it". Three tools answer it from different sides,
|
|
71
|
+
without launching the game:
|
|
72
|
+
|
|
73
|
+
- **`owner_of`**: which mod a file comes from. Give it a DLL named in a
|
|
74
|
+
crash log, a path from `triage_logs`, or a bare file name. It answers
|
|
75
|
+
from the game's own mod folders, Vortex's deployment records and Mod
|
|
76
|
+
Organizer 2's profiles (Skyrim), and says how it decided. For example,
|
|
77
|
+
`owner_of game=cyberpunk2077 paths=["TweakXL.dll"]` names the Vortex mod
|
|
78
|
+
that deployed it.
|
|
79
|
+
- **`who_touches`**: which mods write the same record or entry: Cyberpunk
|
|
80
|
+
2077 TweakDB records and flats, Baldur's Gate 3 stats entries, Stardew
|
|
81
|
+
Valley Content Patcher assets and entries. With no `key` it lists the
|
|
82
|
+
keys mods actually disagree on, for example
|
|
83
|
+
`who_touches game=cyberpunk2077`. These don't count as a disagreement:
|
|
84
|
+
- identical writes;
|
|
85
|
+
- list edits on different items;
|
|
86
|
+
- a mod the game does not load (a BG3 pak missing from
|
|
87
|
+
`modsettings.lsx`);
|
|
88
|
+
- a second copy of the same mod.
|
|
89
|
+
|
|
90
|
+
Copies of one mod are reported separately, under `duplicateCopies`.
|
|
91
|
+
`who_touches key=<record>` shows every writer of one key, and `mod=<name>`
|
|
92
|
+
shows what one mod writes.
|
|
93
|
+
- **`triage_logs`** with `attributionFor`: what the frameworks' own logs say
|
|
94
|
+
happened. For example, TweakXL's log gives the order it read tweak files
|
|
95
|
+
in, and a later file wins the same flat.
|
|
96
|
+
|
|
97
|
+
Together they catch the quiet problems, for example a tweak file installed
|
|
98
|
+
twice and loaded twice, or a stale copy of a mod that the game binds instead
|
|
99
|
+
of the current one. `who_touches` reads files, not the running game, so
|
|
100
|
+
scripts that change things at runtime are not seen. Which of two
|
|
101
|
+
disagreeing mods wins depends on load order, which these tools report but
|
|
102
|
+
do not decide.
|
|
103
|
+
|
|
66
104
|
## Supported games
|
|
67
105
|
|
|
68
|
-
| Surface | Game | Mod systems modeled |
|
|
69
|
-
|
|
70
|
-
| `cyberpunk2077` | Cyberpunk 2077 | legacy archives, REDmod, redscript, TweakXL, ArchiveXL, RED4ext, CET; the TweakDB vanilla index; garment, vehicle, tweak and packaging validators; the CET bridge |
|
|
71
|
-
| `baldursgate3` | Baldur's Gate 3 | `.pak` mods, loose overrides, Script Extender, the native mod loader; the stats, GUID and handle vanilla index; stats, story, progression and visual-bank validators; the Script Extender bridge |
|
|
72
|
-
| `skyrimse` | Skyrim Special Edition | plugins, BSA archives, SKSE plugins, loose files |
|
|
73
|
-
| `eldenring` | Elden Ring | me3 profiles, Mod Engine 2 overlay and DLLs, UXM loose files; paramdef-typed param exports, the `.me3` profile, package-layout and DCX rules |
|
|
74
|
-
| `valheim` | Valheim | BepInEx plugins, patchers, config, MonoMod, the Doorstop loader; Thunderstore packaging, plugin-source and config rules; the vanilla index from Jötunn's dumps |
|
|
75
|
-
| `stardewvalley` | Stardew Valley | SMAPI mods and Content Patcher packs, the load-order override lists, SMAPI's log; the manifest and `content.json` rules; the wiki schema index, built from the wiki's data pages, which `check_toolchain` fetches for you |
|
|
76
|
-
| `subnautica2` | Subnautica 2 | `~mods` paks, LogicMods, UE4SS mods, DLC-style plugins; triplet, mount-point, TOC and UE4SS-shape rules |
|
|
77
|
-
| `nivalisnights` | Nivalis Nights | BepInEx 6 IL2CPP plugins, patchers, config, generated interop, MelonLoader mods; the vanilla index (items, recipes, vendors, venues, NPCs, by GUID) built from your install with Cpp2IL and UnityPy |
|
|
106
|
+
| Surface | Game | Tested | Mod systems modeled |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `cyberpunk2077` | Cyberpunk 2077 | field-tested | legacy archives, REDmod, redscript, TweakXL, ArchiveXL, RED4ext, CET; the TweakDB vanilla index; garment, vehicle, tweak and packaging validators; the CET bridge |
|
|
109
|
+
| `baldursgate3` | Baldur's Gate 3 | field-tested | `.pak` mods, loose overrides, Script Extender, the native mod loader; the stats, GUID and handle vanilla index; stats, story, progression and visual-bank validators; the Script Extender bridge |
|
|
110
|
+
| `skyrimse` | Skyrim Special Edition | research only | plugins, BSA archives, SKSE plugins, loose files |
|
|
111
|
+
| `eldenring` | Elden Ring | research only | me3 profiles, Mod Engine 2 overlay and DLLs, UXM loose files; paramdef-typed param exports, the `.me3` profile, package-layout and DCX rules |
|
|
112
|
+
| `valheim` | Valheim | research only | BepInEx plugins, patchers, config, MonoMod, the Doorstop loader; Thunderstore packaging, plugin-source and config rules; the vanilla index from Jötunn's dumps |
|
|
113
|
+
| `stardewvalley` | Stardew Valley | research only | SMAPI mods and Content Patcher packs, the load-order override lists, SMAPI's log; the manifest and `content.json` rules; the wiki schema index, built from the wiki's data pages, which `check_toolchain` fetches for you |
|
|
114
|
+
| `subnautica2` | Subnautica 2 | research only | `~mods` paks, LogicMods, UE4SS mods, DLC-style plugins; triplet, mount-point, TOC and UE4SS-shape rules |
|
|
115
|
+
| `nivalisnights` | Nivalis Nights | checked on an install | BepInEx 6 IL2CPP plugins, patchers, config, generated interop, MelonLoader mods; the vanilla index (items, recipes, vendors, venues, NPCs, by GUID) built from your install with Cpp2IL and UnityPy |
|
|
116
|
+
|
|
117
|
+
- **Field-tested:** used against real installs, real mod projects and the
|
|
118
|
+
running game, through the in-game bridge, over many field runs.
|
|
119
|
+
- **Checked on an install:** detection, logs, compatibility and the vanilla
|
|
120
|
+
index were checked against an installed copy of the game. No mod project
|
|
121
|
+
has been built or deployed through it yet, and it has no bridge.
|
|
122
|
+
- **Research only:** written from the game's modding documentation, its
|
|
123
|
+
community tools' sources and their published file formats, and tested
|
|
124
|
+
against fixtures. No install was behind it. Expect gaps, and please report
|
|
125
|
+
them.
|
|
78
126
|
|
|
79
127
|
Installs are auto-detected from Steam (every library folder), plus GOG and
|
|
80
128
|
Epic paths where they apply. Override with `installPath` on any tool, or set
|
|
@@ -88,13 +136,15 @@ Epic paths where they apply. Override with `installPath` on any tool, or set
|
|
|
88
136
|
| `detect_install` | Locate one game, and report every mod root and log source, marking which exist. |
|
|
89
137
|
| `list_mods` | Enumerate installed mods across all roots, with sizes, timestamps and parsed manifests. |
|
|
90
138
|
| `inspect_mod` | Full detail on one mod: files owned, manifest, warnings. |
|
|
91
|
-
| `find_conflicts` | Paths claimed by more than one mod, restricted to roots where that is actually possible. |
|
|
139
|
+
| `find_conflicts` | Paths claimed by more than one mod, restricted to roots where that is actually possible. Where Mod Organizer 2 manages the game, it also reports the files one MO2 mod overrides in another. |
|
|
140
|
+
| `owner_of` | Which installed mod a file comes from, and which copy the game sees when several provide it: for a DLL named in a crash log, a path from `triage_logs`, or a bare file name. It reads the game's own mod folders, Mod Organizer 2's profiles (Skyrim) and Vortex's deployment records, and says how each answer was decided. |
|
|
141
|
+
| `who_touches` | Which installed mods write a given record or entry, at the level the game merges on: Cyberpunk 2077 TweakDB records and flats, Baldur's Gate 3 stats entries (read out of paks), Stardew Valley Content Patcher assets and entries. With no key it lists the contested ones. A mod the game does not load is listed but never contests, and copies of one mod count once. Static: it does not decide load order, and runtime scripts are not seen. |
|
|
92
142
|
| `load_order` | Read the game's explicit load order and cross-check it against what is installed. |
|
|
93
143
|
| `load_order_diff` | Snapshot the load order before a launch, then report what the game dropped, added or reordered (BG3 `modsettings.lsx`). |
|
|
94
144
|
| `read_log` | Tail a mod-related log (redscript, RED4ext, CET, SKSE, BG3SE, BepInEx, SMAPI and more). |
|
|
95
|
-
| `triage_logs` | Read every mod log for the current run and classify lines against known failure signatures. It returns one verdict: which systems loaded, what failed first, and the usual fix. |
|
|
145
|
+
| `triage_logs` | Read every mod log for the current run and classify lines against known failure signatures. It returns one verdict: which systems loaded, what failed first, and the usual fix. Where a framework's log names who acted (TweakXL's read order, ArchiveXL's merges), it also says which mod did what; `attributionFor` narrows that to one asset, record or mod. |
|
|
96
146
|
| `check_compat` | Detect the game build, loaders, frameworks and tools, and check them against cited compatibility floors. |
|
|
97
|
-
| `check_toolchain` | Locate the external tools (LSLib divine, WolvenKit CLI, Blender, Cpp2IL and others), report each one's path and version (never guessed), and say what each feature needs. It also remembers a tool or data folder you point it at,
|
|
147
|
+
| `check_toolchain` | Locate the external tools (LSLib divine, WolvenKit CLI, Blender, Cpp2IL and others), report each one's path and version (never guessed), and say what each feature needs. It also remembers a tool or data folder you point it at, installs the tools ModWright can install, and trusts a project to run its own build commands. |
|
|
98
148
|
| `project_info` | Load a `modwright.json` mod project: sources, build, deploy targets, dependencies and publish targets, with paths resolved and structural warnings. |
|
|
99
149
|
| `lookup` | Search the knowledge base of game and toolchain facts (each with a status and a source), the validator catalogue, or the game's vanilla index you built locally. |
|
|
100
150
|
| `build_index` | Extract and index the game's own data into a local SQLite file in ModWright's cache. Never redistributed. |
|
|
@@ -114,6 +164,23 @@ Epic paths where they apply. Override with `installPath` on any tool, or set
|
|
|
114
164
|
Every tool that writes is **dry-run by default**: it describes the plan and
|
|
115
165
|
writes nothing until you apply it.
|
|
116
166
|
|
|
167
|
+
## How much to trust an answer
|
|
168
|
+
|
|
169
|
+
Every fact `lookup` returns carries a status and a source:
|
|
170
|
+
|
|
171
|
+
| Status | Means |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `verified` | Someone observed it in the game; the fact says when. |
|
|
174
|
+
| `community` | A wiki, README, issue or forum post states it, cited by URL, and it has not been re-checked here. |
|
|
175
|
+
| `inferred` | Derived from reading code or from other facts. |
|
|
176
|
+
| `unverified` | Written from documentation or reasoning, and never observed. |
|
|
177
|
+
| `contradicted` | Once believed and later shown wrong. It is kept, pointing at what replaced it, so the mistake is not made again. |
|
|
178
|
+
|
|
179
|
+
A validator that cannot run is reported as **skipped**, with the reason,
|
|
180
|
+
never as passed. That happens when the vanilla index is not built, a tool is
|
|
181
|
+
missing, or the project has nothing of the kind it checks. An in-game test
|
|
182
|
+
whose probe is `unverified` can pass, but its result stays below `verified`.
|
|
183
|
+
|
|
117
184
|
## Workflow prompts
|
|
118
185
|
|
|
119
186
|
Seven workflow prompts walk the authoring loop in any MCP client
|
|
@@ -173,6 +240,12 @@ project:
|
|
|
173
240
|
`check_toolchain action=install tool=cpp2il` shows exactly what it would
|
|
174
241
|
fetch (URL, size, sha256, destination); `mode=apply` installs it. Downloads
|
|
175
242
|
are pinned and checked before anything is written.
|
|
243
|
+
- **Trusted projects** are listed in `trusted-projects.json`, beside that
|
|
244
|
+
config. A project's `exec` build steps and `toolchain` overrides run only
|
|
245
|
+
after you trust it once: `check_toolchain action=trust projectPath=<mod>`
|
|
246
|
+
lists what it would allow, and `mode=apply` records it. Until then an
|
|
247
|
+
applied build, convert, template extract or index build that would run
|
|
248
|
+
them is refused. `MODWRIGHT_TRUST_ALL=1` trusts every project, for CI.
|
|
176
249
|
- **Overrides:** `MODWRIGHT_CACHE_DIR` moves the cache; `MODWRIGHT_HOME`
|
|
177
250
|
moves everything.
|
|
178
251
|
- **Project state** stays in the project's own `.modwright/`.
|
|
@@ -205,6 +278,41 @@ produce confident nonsense:
|
|
|
205
278
|
- **Redistributing game data.** Indexes and templates are built from your
|
|
206
279
|
own install, on your machine, and never shipped.
|
|
207
280
|
|
|
281
|
+
## Security
|
|
282
|
+
|
|
283
|
+
ModWright runs programs on your machine and writes into your game folders.
|
|
284
|
+
What it runs, and where it writes:
|
|
285
|
+
|
|
286
|
+
- **Tools it finds.** It runs the external tools it locates (LSLib divine,
|
|
287
|
+
WolvenKit CLI, Blender and the others `check_toolchain` lists). They come
|
|
288
|
+
from a `MODWRIGHT_TOOL_*` variable, your per-user config, its own managed
|
|
289
|
+
installs, PATH or common install locations. Managed installs are pinned
|
|
290
|
+
and their sha256 is checked before anything is written.
|
|
291
|
+
- **A project's own commands.** A mod project's `exec` build steps and its
|
|
292
|
+
`toolchain` overrides are programs the project's author chose. They run
|
|
293
|
+
only after you trust that project on this machine:
|
|
294
|
+
`check_toolchain action=trust projectPath=<mod>` lists what it would allow,
|
|
295
|
+
and `mode=apply` records it. Read the project's `modwright.json` first,
|
|
296
|
+
above all for a mod you cloned from someone else. Trust belongs to the
|
|
297
|
+
folder, not to the file's content, so a trusted project that later gains
|
|
298
|
+
a step (after a `git pull`) is not asked again.
|
|
299
|
+
- **Where it writes.** Every writer is dry-run until you apply it, and it
|
|
300
|
+
backs up what it replaces. A build writes under the project's
|
|
301
|
+
`build.outDir`, and a deploy writes under the game's mod roots. A project
|
|
302
|
+
path that is absolute or climbs out with `..` is refused unless that entry
|
|
303
|
+
sets `"allowOutsideRoot": true`. `rollback` restores only under the
|
|
304
|
+
project and the game's mod roots, unless you pass `allowOutsideRoots`.
|
|
305
|
+
- **The in-game bridges.** While a verification bridge is deployed and the
|
|
306
|
+
game runs, anything that can write the bridge's request folder can run Lua
|
|
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
|
|
309
|
+
build that contains it). The bridge READMEs under `bridges/` describe each
|
|
310
|
+
one's trust model.
|
|
311
|
+
|
|
312
|
+
To report a security problem, use **Report a vulnerability** on
|
|
313
|
+
[modwright-community's Security tab](https://github.com/jtrachtenberg/modwright-community/security),
|
|
314
|
+
not a public issue.
|
|
315
|
+
|
|
208
316
|
## Reporting a problem
|
|
209
317
|
|
|
210
318
|
Bug reports, game and feature requests and questions go to
|
package/bridges/bg3-se/README.md
CHANGED
|
@@ -82,6 +82,21 @@ No Lua parser is vendored. Syntax is checked with `luac -p` under Lua 5.1 and
|
|
|
82
82
|
dispatch-table keys equal the catalogue's probe ids, that the `PROTOCOL_VERSION` literals
|
|
83
83
|
agree, that `bridge.json` lists every shipped file, and that every `.lua` carries the banner.
|
|
84
84
|
|
|
85
|
+
## Trust model
|
|
86
|
+
|
|
87
|
+
While this bridge is deployed and the game is running, anything that can write a file into
|
|
88
|
+
its request folder (`%LOCALAPPDATA%\Larian Studios\Baldur's Gate 3\Script Extender`) can run
|
|
89
|
+
Lua inside the game. A request names its own probes and its own grants, and `bg3.lua.eval`
|
|
90
|
+
runs whatever code a request carries once the request grants the `session` tier. There is no
|
|
91
|
+
token. The nonce and the request's `armedUntil` expiry only stop a consumed or stale request
|
|
92
|
+
from running again; they do not say who wrote it. So:
|
|
93
|
+
|
|
94
|
+
- deploy the bridge only to a machine and a profile you control, and only while you are
|
|
95
|
+
verifying a mod;
|
|
96
|
+
- remove `Mods/ModWrightBridge` from the profile when you are done, before playing normally
|
|
97
|
+
or sharing the profile;
|
|
98
|
+
- never ship it. `bg3.history.bridge-never-ships` blocks a release build that contains it.
|
|
99
|
+
|
|
85
100
|
## What this bridge never does
|
|
86
101
|
|
|
87
102
|
- No Osiris **action**, ever, and no forced cast: `Osi.UseSpell` was observed to re-cast,
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
-- (verified, 2.31, observed in game 2026-08-16)
|
|
40
40
|
-- TweakDB:GetRecord / TweakDB:GetFlat observed in the CET console on
|
|
41
41
|
-- 2026-09-08 (see the rows below).
|
|
42
|
+
-- obj:GetNPCsAroundObject(radius) the game's RTTI dump (rayshader/cp2077-nativedb,
|
|
43
|
+
-- obj:GetEntitiesAroundObject(radius, filter) gameObject methods); no CET mod or wiki page calls
|
|
44
|
+
-- TSF_Or / TSF_Any / Enum.new them — `unverified`; CET's source pins the Lua side
|
|
42
45
|
--
|
|
43
46
|
-- MUTATION. Two rows have effect `mutate`: `cp2077.inventory.add`, and `cp2077.lua.eval`
|
|
44
47
|
-- (added 2026-09-11). They are the two handlers in this
|
|
@@ -816,6 +819,116 @@ Probes.handlers = {
|
|
|
816
819
|
end,
|
|
817
820
|
},
|
|
818
821
|
|
|
822
|
+
-- World-placed object verification — **unverified**, written 2026-09-10 with no game on
|
|
823
|
+
-- hand and never run in game. `gameObject` (parent `entGameEntity`) carries
|
|
824
|
+
-- `GetNPCsAroundObject(range: Float) -> array<ref<NPCPuppet>>` and
|
|
825
|
+
-- `GetEntitiesAroundObject(range: Float, searchFilter: gameTargetSearchFilter) -> array<ref<entEntity>>`
|
|
826
|
+
-- in the shipped game's own RTTI (rayshader/cp2077-nativedb at b5d29af, game 2.31), so both
|
|
827
|
+
-- exist — stronger evidence than "nothing on disk", which is why this row exists instead of
|
|
828
|
+
-- sitting out like `cp2077.loot.roll`. Neither a shipped CET mod nor a wiki page calls either
|
|
829
|
+
-- one from Lua, but CET's own source (yamashi/CyberEngineTweaks 9a8522f; knowledge facts
|
|
830
|
+
-- cp2077.cet.*) settles the calling convention:
|
|
831
|
+
-- * the TSF_* filter constructors are exposed as bare globals, `Game.TSF_X` and
|
|
832
|
+
-- `Game["TSF_X;"]`; a returned struct is a ClassReference copy that passes straight back
|
|
833
|
+
-- as a struct parameter (no layout is ever guessed);
|
|
834
|
+
-- * an enum parameter accepts a number, a member name or `Enum.new(type, name)`, but an
|
|
835
|
+
-- UNDECLARED value is coerced to 0 silently — so no ORed masks; declared members only,
|
|
836
|
+
-- combined with TSF_Or, and `Enum.new` with the full type name so a mistake is loud;
|
|
837
|
+
-- * an array return is a 1-based table whose null handles are nil holes — count with
|
|
838
|
+
-- pairs, never `#`.
|
|
839
|
+
-- Two shapes, chosen by `params.filter`:
|
|
840
|
+
-- "npc" (default): `GetNPCsAroundObject(radius)`, one scalar argument, the same risk class
|
|
841
|
+
-- as `ent:GetWorldPosition()` and `GetTaggedIDs(tag)` above.
|
|
842
|
+
-- "objects": `GetEntitiesAroundObject(radius, TSF_Or(TSF_Any(Obj_Device), TSF_Any(Obj_Other),
|
|
843
|
+
-- TSF_Any(Obj_Puppet), TSF_Any(Obj_Sensor)))` — the call that could verify a placed static
|
|
844
|
+
-- prop (a loot-container template carrying a gameTargetingComponent is plausibly in the
|
|
845
|
+
-- targeting set). TSF_Any/TSF_Or semantics and whether the prop is in the set are exactly
|
|
846
|
+
-- what a live run observes; every step is pcall-wrapped and reported, never swallowed.
|
|
847
|
+
-- The anchor defaults to the player; `params.tag`/`params.entityRef` pick another, exactly as
|
|
848
|
+
-- `resolveEntity` does for the rows above.
|
|
849
|
+
["cp2077.entity.nearby"] = {
|
|
850
|
+
attestation = "unverified (RTTI dump confirms gameObject::GetNPCsAroundObject/GetEntitiesAroundObject exist; CET source pins the Lua calling convention; no live call observed yet)",
|
|
851
|
+
citation = "rayshader/cp2077-nativedb classes.json at b5d29af (gameObject::GetNPCsAroundObject;Float, GetEntitiesAroundObject;FloatTargetSearchFilter); " ..
|
|
852
|
+
"yamashi/CyberEngineTweaks 9a8522f (RTTI binding)",
|
|
853
|
+
effect = "read",
|
|
854
|
+
run = function(probe)
|
|
855
|
+
local p = params(probe)
|
|
856
|
+
local radius = tonumber(p.radius)
|
|
857
|
+
if radius == nil or radius <= 0 then return errored("params.radius is required and must be a positive number") end
|
|
858
|
+
local filter = p.filter == nil and "npc" or tostring(p.filter)
|
|
859
|
+
if filter ~= "npc" and filter ~= "objects" then
|
|
860
|
+
return errored("params.filter must be \"npc\" (default) or \"objects\", got " .. tostring(p.filter))
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
local anchorParams = p
|
|
864
|
+
if p.tag == nil and p.entityRef == nil then anchorParams = { entityRef = "$player" } end
|
|
865
|
+
local ent, describe, e = resolveEntity(anchorParams)
|
|
866
|
+
if ent == nil then return errored(e or "could not resolve an anchor entity", { resolve = describe }) end
|
|
867
|
+
|
|
868
|
+
local evidence = { resolve = describe, radius = radius, filter = filter }
|
|
869
|
+
|
|
870
|
+
local list
|
|
871
|
+
if filter == "npc" then
|
|
872
|
+
evidence.attemptedCall = "GetNPCsAroundObject(radius) — NPCs only; a static prop like a placed item does not " ..
|
|
873
|
+
"show up here even when it does exist"
|
|
874
|
+
local okCall, callErr = pcall(function() list = ent:GetNPCsAroundObject(radius) end)
|
|
875
|
+
if not okCall then
|
|
876
|
+
return errored("obj:GetNPCsAroundObject(" .. tostring(radius) .. ") raised an error (recorded rather " ..
|
|
877
|
+
"than swallowed, since this row is unverified): " .. tostring(callErr), evidence)
|
|
878
|
+
end
|
|
879
|
+
else
|
|
880
|
+
evidence.attemptedCall = "GetEntitiesAroundObject(radius, TSF_Or(TSF_Any(Obj_Device), TSF_Any(Obj_Other), " ..
|
|
881
|
+
"TSF_Any(Obj_Puppet), TSF_Any(Obj_Sensor))) — each member via Enum.new so a wrong name is an error, " ..
|
|
882
|
+
"never a silent 0"
|
|
883
|
+
local searchFilter
|
|
884
|
+
local okFilter, filterErr = pcall(function()
|
|
885
|
+
local function member(name) return Enum.new("gametargetingSystemSearchFilterMaskValue", name) end
|
|
886
|
+
searchFilter = TSF_Or(TSF_Any(member("Obj_Device")), TSF_Any(member("Obj_Other")),
|
|
887
|
+
TSF_Any(member("Obj_Puppet")), TSF_Any(member("Obj_Sensor")))
|
|
888
|
+
end)
|
|
889
|
+
if not okFilter or searchFilter == nil then
|
|
890
|
+
return errored("building the gameTargetSearchFilter raised an error or returned nil (TSF_*/Enum.new " ..
|
|
891
|
+
"exposure is what this settles): " .. tostring(filterErr), evidence)
|
|
892
|
+
end
|
|
893
|
+
local okCall, callErr = pcall(function() list = ent:GetEntitiesAroundObject(radius, searchFilter) end)
|
|
894
|
+
if not okCall then
|
|
895
|
+
return errored("obj:GetEntitiesAroundObject(" .. tostring(radius) .. ", filter) raised an error (recorded " ..
|
|
896
|
+
"rather than swallowed, since this row is unverified): " .. tostring(callErr), evidence)
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
if list == nil then
|
|
900
|
+
return errored(evidence.attemptedCall:match("^[%w]+") .. " returned nil — the RTTI method exists but this " ..
|
|
901
|
+
"call did not resolve here; a live run is needed to say why", evidence)
|
|
902
|
+
end
|
|
903
|
+
if type(list) ~= "table" then
|
|
904
|
+
return errored("expected a table from the array return, got " .. type(list), evidence)
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
-- CET returns an array as a 1-based table, but a null handle inside it becomes a nil
|
|
908
|
+
-- hole, so `#` and ipairs can stop early: walk with pairs and count what is there.
|
|
909
|
+
local count, holes = 0, 0
|
|
910
|
+
local out = Protocol.array({})
|
|
911
|
+
local slots = 0
|
|
912
|
+
for k, _ in pairs(list) do if type(k) == "number" and k > slots then slots = k end end
|
|
913
|
+
for i = 1, slots do
|
|
914
|
+
local other = list[i]
|
|
915
|
+
if other == nil then
|
|
916
|
+
holes = holes + 1
|
|
917
|
+
else
|
|
918
|
+
count = count + 1
|
|
919
|
+
local okName, name = pcall(function() return other:GetClassName() end)
|
|
920
|
+
local entry = { className = (okName and name ~= nil) and tostring(name) or nil }
|
|
921
|
+
local okApp, app = pcall(function() return other:GetCurrentAppearanceName() end)
|
|
922
|
+
if okApp and app ~= nil then entry.appearance = tostring(app) end
|
|
923
|
+
out[#out + 1] = entry
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
evidence.entities = out
|
|
927
|
+
evidence.nilHoles = holes
|
|
928
|
+
return ok(count, evidence)
|
|
929
|
+
end,
|
|
930
|
+
},
|
|
931
|
+
|
|
819
932
|
-- Row 10. `TweakDB:GetRecord(id)`, not attested by any page or mod on disk, but observed
|
|
820
933
|
-- to work from the CET console (2026-09-08):
|
|
821
934
|
-- print(tostring(TweakDB:GetRecord("Items.Preset_Example")))
|
|
@@ -111,7 +111,7 @@ be diagnosed.
|
|
|
111
111
|
|
|
112
112
|
Fenced with `-- MODWRIGHT-PROBE-TABLE-BEGIN` / `-- MODWRIGHT-PROBE-TABLE-END` in
|
|
113
113
|
`probes.lua`, so the parity test reads only the table and never an id that appears in a
|
|
114
|
-
comment.
|
|
114
|
+
comment. Fifteen ids, one per catalogue row, each carrying that row's own status word:
|
|
115
115
|
|
|
116
116
|
| id | Status | If its call is missing |
|
|
117
117
|
|---|---|---|
|
|
@@ -120,10 +120,12 @@ comment. Thirteen ids, one per catalogue row, each carrying that row's own statu
|
|
|
120
120
|
| `cp2077.player.present` | attested(engine) | `error` if `Game` is absent |
|
|
121
121
|
| `cp2077.item.equipped` | attested(engine) | — |
|
|
122
122
|
| `cp2077.inventory.add` | attested(doc), **mutate** | `error`; skipped without `allowMutate` |
|
|
123
|
+
| `cp2077.lua.eval` | **mutate**; runs a request's Lua chunk | skipped without the `session` tier |
|
|
123
124
|
| `cp2077.entity.tagged` | attested(engine) | — |
|
|
124
125
|
| `cp2077.entity.components` | attested(engine) for the call; names unverified | `error` without `params.names` |
|
|
125
126
|
| `cp2077.entity.transform` | attested(engine) for item objects; unverified for a sector prop | says which, in `evidence.attestationForThisCall` |
|
|
126
127
|
| `cp2077.entity.appearance` | unverified (composite) | `skipped` without a baseline |
|
|
128
|
+
| `cp2077.entity.nearby` | unverified — the RTTI methods exist; never run in game | `error` naming the call that raised or returned nil |
|
|
127
129
|
| `cp2077.tweakdb.record` | unverified | `error` |
|
|
128
130
|
| `cp2077.tweakdb.flat` | unverified | `error` |
|
|
129
131
|
| `cp2077.interaction.present` | unverified (the component *name*) | `error` without `params.names` |
|
|
@@ -155,6 +157,20 @@ The Lua is kept inside the 5.1-compatible subset shipped CET mods use, and check
|
|
|
155
157
|
against both dialects — CET embeds a 5.4-era sol2, but nothing on disk pins which dialect
|
|
156
158
|
features are available, so the intersection is the safe target.
|
|
157
159
|
|
|
160
|
+
## Trust model
|
|
161
|
+
|
|
162
|
+
While this bridge is deployed and the game is running, anything that can write a file into
|
|
163
|
+
its write root (the folders listed under "The `io` write-root discovery") can run Lua inside
|
|
164
|
+
the game, and so can anything that can type into the CET console. A request names its own
|
|
165
|
+
probes and its own grants, and `cp2077.lua.eval` runs whatever code a request carries once
|
|
166
|
+
the request grants the `session` tier. There is no token. The nonce and the request's expiry
|
|
167
|
+
only stop a consumed or stale request from running again; they do not say who wrote it. So:
|
|
168
|
+
|
|
169
|
+
- deploy the bridge only to an install you control, and only while you are verifying a mod;
|
|
170
|
+
- remove `bin/x64/plugins/cyber_engine_tweaks/mods/ModWrightBridge` when you are done,
|
|
171
|
+
before playing normally;
|
|
172
|
+
- never ship it. `cp2077.history.bridge-never-ships` blocks a release build that contains it.
|
|
173
|
+
|
|
158
174
|
## What this bridge never does
|
|
159
175
|
|
|
160
176
|
* It never observes: no CET listener API is attested, so `effect: "observe"` is `unsupported`
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "ModWrightBridge/probes.lua",
|
|
46
|
-
"sha256": "
|
|
46
|
+
"sha256": "169a31b158edcefda6721c3b90656c59fd9f7d7bf928a96d03ef3645a5eb1991"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "ModWrightBridge/protocol.lua",
|
|
@@ -51,7 +51,14 @@ export async function readPendingArm(projectRoot, id) {
|
|
|
51
51
|
catch {
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
let json;
|
|
55
|
+
try {
|
|
56
|
+
json = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const parsed = pendingArmSchema.safeParse(json);
|
|
55
62
|
return parsed.success ? parsed.data : undefined;
|
|
56
63
|
}
|
|
57
64
|
export async function listPendingArms(projectRoot, game, opts) {
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { isDirectory, pathExists } from "../fsutil.js";
|
|
5
5
|
import { parseProject } from "../project/schema.js";
|
|
6
6
|
import { PROJECT_FILE } from "../project/types.js";
|
|
7
|
-
import { executePlan, planCopy, planFile, planMkdir } from "../safety/index.js";
|
|
7
|
+
import { executePlan, planCopy, planDelete, planFile, planMkdir } from "../safety/index.js";
|
|
8
8
|
const BRIDGE_DIR_BY_GAME = {
|
|
9
9
|
baldursgate3: "bg3-se",
|
|
10
10
|
cyberpunk2077: "cp2077-cet",
|
|
@@ -77,7 +77,7 @@ export async function planStageBridge(projectRoot, game) {
|
|
|
77
77
|
}
|
|
78
78
|
const to = stagedBridgeDir(projectRoot, game);
|
|
79
79
|
const registration = await planDeployTargetRegistration(projectRoot, game);
|
|
80
|
-
const operations = [planMkdir(path.dirname(to)), planCopy(from, to)];
|
|
80
|
+
const operations = [planMkdir(path.dirname(to)), ...(await pathExists(to) ? [planDelete(to)] : []), planCopy(from, to)];
|
|
81
81
|
if (registration.op)
|
|
82
82
|
operations.push(registration.op);
|
|
83
83
|
return {
|
package/dist/core/build/run.js
CHANGED
|
@@ -2,7 +2,9 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { pathExists, walk } from "../fsutil.js";
|
|
4
4
|
import { normalizeBuildSteps } from "../project/build-steps.js";
|
|
5
|
+
import { outsideRootPaths } from "../project/outside-root.js";
|
|
5
6
|
import { locateTool as coreLocateTool, TOOL_SPECS } from "../toolchain/index.js";
|
|
7
|
+
import { describeNeeds, isProjectTrusted, trustRefusal } from "../trust.js";
|
|
6
8
|
import { buildContext, runValidators } from "../validate/index.js";
|
|
7
9
|
import { runAssertAbsentStep, runAssertPresentStep, runExecStep, runZipStep } from "./steps/core.js";
|
|
8
10
|
import { runBlenderExportStep, runVerifyArchiveListingStep, runWolvenkitImportStep, runWolvenkitPackStep, } from "./steps/cyberpunk.js";
|
|
@@ -168,6 +170,18 @@ export async function runBuild(project, surface, options) {
|
|
|
168
170
|
throw new Error(`--only named no matching step id(s): ${options.only.join(", ")}. Known ids: ${active.map(({ step }) => step.id).filter(Boolean).join(", ") || "(none declared)"}`);
|
|
169
171
|
}
|
|
170
172
|
const skippedVariant = gatedOut.map(stepName);
|
|
173
|
+
const outside = outsideRootPaths(project.project, selected.map(({ step }) => step)).filter((e) => e.kind === "build-step" && !e.allowed);
|
|
174
|
+
if (outside.length > 0) {
|
|
175
|
+
throw new Error(`${project.project.name}: ${outside.map((e) => e.message).join("; ")}. A build writes only under build.outDir and runs only inside the project; ` +
|
|
176
|
+
`set "allowOutsideRoot": true on the step if this is intended.`);
|
|
177
|
+
}
|
|
178
|
+
if (options.mode === "apply") {
|
|
179
|
+
const exec = selected.flatMap(({ step, stepIndex }) => (step.step === "exec" ? [`"${stepName({ step, stepIndex })}" (${step.command})`] : []));
|
|
180
|
+
const what = describeNeeds({ toolchain: Object.keys(project.project.toolchain ?? {}), exec });
|
|
181
|
+
if (what && !(await (options.isTrusted ?? ((root) => isProjectTrusted(root)))(project.root))) {
|
|
182
|
+
throw new Error(trustRefusal(project.root, what));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
171
185
|
const version = options.version ?? project.project.version ?? "0.0.0";
|
|
172
186
|
const stagingDir = project.build.staging
|
|
173
187
|
? path.resolve(project.root, project.build.staging)
|
package/dist/core/build/stage.js
CHANGED
|
@@ -96,12 +96,13 @@ async function planFiles(step, ctx) {
|
|
|
96
96
|
};
|
|
97
97
|
const isIncluded = (relForMatch) => includeRes.length === 0 || includeRes.some((re) => re.test(relForMatch));
|
|
98
98
|
const add = (abs, relDest) => {
|
|
99
|
-
const
|
|
99
|
+
const key = relDest.toLowerCase();
|
|
100
|
+
const existing = claimed.get(key);
|
|
100
101
|
if (existing !== undefined && existing !== origin) {
|
|
101
102
|
throw new BuildStepError(`stage: ${existing} and ${origin} both write "${relDest}" — one would silently overlay the other. ` +
|
|
102
103
|
`Give one of them its own "into" prefix.`, step.id, step.step);
|
|
103
104
|
}
|
|
104
|
-
claimed.set(
|
|
105
|
+
claimed.set(key, origin);
|
|
105
106
|
files.push({ abs, relDest, origin });
|
|
106
107
|
};
|
|
107
108
|
if (entry.output !== undefined) {
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { isDirectory, walk } from "../../fsutil.js";
|
|
4
4
|
import { globToRegExp } from "../../validate/context.js";
|
|
5
5
|
import { runTool } from "../../toolchain/run.js";
|
|
6
|
+
import { DEFAULT_TOOL_TIMEOUT_MS } from "../../toolchain/types.js";
|
|
6
7
|
import { TOOL_SPECS } from "../../toolchain/specs.js";
|
|
7
8
|
import { directoryEntry, pathEntries } from "../manifest.js";
|
|
8
9
|
import { resolveOutDirPath, resolveSourceOrStagePath, resolveStageId } from "../paths.js";
|
|
@@ -86,9 +87,8 @@ export async function runZipStep(step, ctx) {
|
|
|
86
87
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
87
88
|
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
88
89
|
await writeZipFile(out, entries.map((entry) => ({ name: entry.name, read: () => fs.readFile(entry.abs) })));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
throw new BuildStepError(`zip: wrote an empty archive: ${out}`, step.id, step.step);
|
|
90
|
+
if (entries.length === 0)
|
|
91
|
+
throw new BuildStepError(`zip: nothing to archive under ${from}`, step.id, step.step);
|
|
92
92
|
const outputs = await pathEntries([out]);
|
|
93
93
|
return {
|
|
94
94
|
inputs: [await directoryEntry(from)],
|
|
@@ -128,7 +128,7 @@ async function resolveCommandLocation(step, ctx) {
|
|
|
128
128
|
}
|
|
129
129
|
return { id: "exec", label, found: true, path: resolveSourceOrStagePath(step.command, ctx), usedBy: [] };
|
|
130
130
|
}
|
|
131
|
-
export const EXEC_DEFAULT_TIMEOUT_MS =
|
|
131
|
+
export const EXEC_DEFAULT_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS;
|
|
132
132
|
export async function runExecStep(step, ctx) {
|
|
133
133
|
const location = await resolveCommandLocation(step, ctx);
|
|
134
134
|
const command = location.path;
|
package/dist/core/deploy/plan.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { pathExists, walk } from "../fsutil.js";
|
|
3
|
+
import { isDirectory, pathExists, walk } from "../fsutil.js";
|
|
4
4
|
import { globToRegExp, isExcludedByGlobs } from "../validate/index.js";
|
|
5
|
+
import { outsideRootPaths } from "../project/outside-root.js";
|
|
5
6
|
import { findRunningProcesses } from "../process.js";
|
|
6
7
|
import { executePlan, isIdenticalFile, planCopy, planDelete, planJunction, planMkdir, planUnlink, sha256File } from "../safety/index.js";
|
|
7
8
|
import { classifyReload } from "./reload.js";
|
|
@@ -158,6 +159,60 @@ export async function planDeploy(inputs) {
|
|
|
158
159
|
backupRoot,
|
|
159
160
|
};
|
|
160
161
|
}
|
|
162
|
+
if (inputs.variant !== undefined) {
|
|
163
|
+
const named = new Set([
|
|
164
|
+
...deploy.targets.flatMap((t) => (t.variant ? [t.variant] : [])),
|
|
165
|
+
...Object.keys(project.project.build?.variants ?? {}),
|
|
166
|
+
]);
|
|
167
|
+
if (!named.has(inputs.variant)) {
|
|
168
|
+
const known = [...named].sort();
|
|
169
|
+
return {
|
|
170
|
+
blocked: [
|
|
171
|
+
{
|
|
172
|
+
code: "unknown-variant",
|
|
173
|
+
message: `${project.project.name}: variant "${inputs.variant}" is named by no deploy target and no build.variants entry` +
|
|
174
|
+
(known.length > 0 ? ` (known: ${known.join(", ")})` : " (this project declares none)") +
|
|
175
|
+
"; nothing was deployed rather than silently deploying only the ungated targets.",
|
|
176
|
+
detail: { variant: inputs.variant, known },
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
confidence: { level: "high", assumptions: [], verifiedBy: ["modwright.json"] },
|
|
180
|
+
targets: [],
|
|
181
|
+
skipped: [],
|
|
182
|
+
deferred: [],
|
|
183
|
+
junctions: [],
|
|
184
|
+
pruned: [],
|
|
185
|
+
skippedDestDirs: [],
|
|
186
|
+
excluded: [],
|
|
187
|
+
warnings,
|
|
188
|
+
writtenPaths: [],
|
|
189
|
+
processCheck: emptyProcessCheck,
|
|
190
|
+
backupRoot,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const outside = outsideRootPaths(project.project).filter((e) => (e.kind === "deploy-target" || e.kind === "junction") && !e.allowed);
|
|
195
|
+
if (outside.length > 0) {
|
|
196
|
+
return {
|
|
197
|
+
blocked: outside.map((e) => ({
|
|
198
|
+
code: "outside-root",
|
|
199
|
+
message: `${e.message}; a deploy writes only under the game's mod roots. Set "allowOutsideRoot": true on that entry if this is intended.`,
|
|
200
|
+
detail: { kind: e.kind, value: e.value },
|
|
201
|
+
})),
|
|
202
|
+
confidence: { level: "high", assumptions: [], verifiedBy: ["modwright.json"] },
|
|
203
|
+
targets: [],
|
|
204
|
+
skipped: [],
|
|
205
|
+
deferred: [],
|
|
206
|
+
junctions: [],
|
|
207
|
+
pruned: [],
|
|
208
|
+
skippedDestDirs: [],
|
|
209
|
+
excluded: [],
|
|
210
|
+
warnings,
|
|
211
|
+
writtenPaths: [],
|
|
212
|
+
processCheck: emptyProcessCheck,
|
|
213
|
+
backupRoot,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
161
216
|
const roots = inputs.roots ?? (inputs.install ? surface.modRoots(inputs.install) : undefined);
|
|
162
217
|
if (!roots) {
|
|
163
218
|
return {
|
|
@@ -202,6 +257,7 @@ export async function planDeploy(inputs) {
|
|
|
202
257
|
}
|
|
203
258
|
if (target.mode && target.mode !== deployMode) {
|
|
204
259
|
skipped.push({ rootId: target.rootId, from: target.from, reason: `target is mode "${target.mode}", this deploy is mode "${deployMode}"` });
|
|
260
|
+
variantExcludedTargets.add(target);
|
|
205
261
|
const skippedRoot = roots.find((r) => r.id === target.rootId);
|
|
206
262
|
if (skippedRoot && (target.locked ?? defaultLocked(target.rootId))) {
|
|
207
263
|
notDeployedLocked.push({ target, rootPath: skippedRoot.path });
|
|
@@ -365,7 +421,18 @@ export async function planDeploy(inputs) {
|
|
|
365
421
|
}
|
|
366
422
|
}
|
|
367
423
|
else {
|
|
424
|
+
const fold = (p) => (process.platform === "win32" ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
368
425
|
for (const j of junctions) {
|
|
426
|
+
if (!(await isDirectory(j.target))) {
|
|
427
|
+
blocked.push({
|
|
428
|
+
code: "junction-target-missing",
|
|
429
|
+
message: `deploy.junctions: ${j.linkPath} would point at ${j.target}, which is not a directory. Build or create it first, or fix the junction's target.`,
|
|
430
|
+
detail: { linkPath: j.linkPath, target: j.target },
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (j.before === "link" && j.linkTargetBefore !== undefined && fold(j.linkTargetBefore) === fold(j.target))
|
|
435
|
+
continue;
|
|
369
436
|
if (j.before === "directory" || j.before === "file") {
|
|
370
437
|
warnings.push(`${j.linkPath} is a real ${j.before}, not a link. It will be backed up and replaced by a link to ${j.target}.`);
|
|
371
438
|
}
|
|
@@ -331,9 +331,11 @@ export function classifyReload(surfaceId, project, writtenPaths, options = {}) {
|
|
|
331
331
|
.map((c) => RANK[c.action]);
|
|
332
332
|
if (unclassified.length > 0)
|
|
333
333
|
ranked.push(RANK.restart);
|
|
334
|
-
const overall =
|
|
335
|
-
? "
|
|
336
|
-
:
|
|
334
|
+
const overall = writtenPaths.length === 0
|
|
335
|
+
? "none"
|
|
336
|
+
: ranked.length === 0
|
|
337
|
+
? "unknown"
|
|
338
|
+
: Object.keys(RANK).find((a) => RANK[a] === Math.max(...ranked));
|
|
337
339
|
const notes = [];
|
|
338
340
|
if (writtenPaths.length === 0) {
|
|
339
341
|
notes.push("nothing was written, so there is nothing to reload");
|
package/dist/core/ini.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseIni(text) {
|
|
2
|
+
const out = new Map();
|
|
3
|
+
let section = "";
|
|
4
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
5
|
+
const line = raw.trim();
|
|
6
|
+
if (!line || line.startsWith(";") || line.startsWith("#"))
|
|
7
|
+
continue;
|
|
8
|
+
const sec = /^\[(.+)\]$/.exec(line);
|
|
9
|
+
if (sec) {
|
|
10
|
+
section = sec[1].trim().toLowerCase();
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const eq = line.indexOf("=");
|
|
14
|
+
if (eq < 0)
|
|
15
|
+
continue;
|
|
16
|
+
const key = line.slice(0, eq).trim().toLowerCase();
|
|
17
|
+
let value = line.slice(eq + 1).trim();
|
|
18
|
+
const wrapped = /^@ByteArray\((.*)\)$/.exec(value);
|
|
19
|
+
if (wrapped)
|
|
20
|
+
value = wrapped[1];
|
|
21
|
+
if (!out.has(section))
|
|
22
|
+
out.set(section, new Map());
|
|
23
|
+
out.get(section).set(key, value);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|