modwright 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +212 -0
- package/assets/claude/agents/mw-explore.md +46 -0
- package/assets/claude/agents/mw-fixture.md +33 -0
- package/assets/claude/agents/mw-rule-author.md +51 -0
- package/assets/claude/agents/mw-scaffold-author.md +48 -0
- package/assets/claude/skills/mw-author.md +59 -0
- package/assets/claude/skills/mw-new-item.md +51 -0
- package/assets/claude/skills/mw-new-spell.md +50 -0
- package/assets/claude/skills/mw-scaffold.md +59 -0
- package/assets/claude/skills/mw-ship.md +57 -0
- package/assets/claude/skills/mw-validate.md +51 -0
- package/assets/claude/skills/mw-verify.md +59 -0
- package/assets/nivalisnights/extract_resources.py +177 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Config.json +5 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapClient.lua +55 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/BootstrapServer.lua +81 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Log.lua +96 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Probes.lua +1038 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/ProbesClient.lua +74 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Protocol.lua +985 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/ScriptExtender/Lua/ModWrightBridge/Watch.lua +175 -0
- package/bridges/bg3-se/Mods/ModWrightBridge/meta.lsx +45 -0
- package/bridges/bg3-se/README.md +99 -0
- package/bridges/bg3-se/bridge.json +68 -0
- package/bridges/cp2077-cet/ModWrightBridge/init.lua +375 -0
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +1104 -0
- package/bridges/cp2077-cet/ModWrightBridge/protocol.lua +1306 -0
- package/bridges/cp2077-cet/README.md +165 -0
- package/bridges/cp2077-cet/bridge.json +53 -0
- package/dist/core/bridge/arm.js +175 -0
- package/dist/core/bridge/cet-console.js +26 -0
- package/dist/core/bridge/cet-log.js +362 -0
- package/dist/core/bridge/index.js +11 -0
- package/dist/core/bridge/locate.js +35 -0
- package/dist/core/bridge/paths.js +34 -0
- package/dist/core/bridge/pending.js +89 -0
- package/dist/core/bridge/poll.js +155 -0
- package/dist/core/bridge/stage.js +100 -0
- package/dist/core/bridge/status.js +97 -0
- package/dist/core/bridge/types.js +145 -0
- package/dist/core/bridge/validate.js +107 -0
- package/dist/core/build/index.js +6 -0
- package/dist/core/build/manifest.js +47 -0
- package/dist/core/build/paths.js +32 -0
- package/dist/core/build/run.js +342 -0
- package/dist/core/build/stage.js +265 -0
- package/dist/core/build/steps/bg3.js +129 -0
- package/dist/core/build/steps/core.js +195 -0
- package/dist/core/build/steps/cyberpunk.js +95 -0
- package/dist/core/build/steps/listing.js +35 -0
- package/dist/core/build/types.js +13 -0
- package/dist/core/build/zip.js +148 -0
- package/dist/core/compat/check.js +113 -0
- package/dist/core/compat/pe.js +220 -0
- package/dist/core/compat/types.js +1 -0
- package/dist/core/compat/version.js +70 -0
- package/dist/core/deploy/guards.js +292 -0
- package/dist/core/deploy/index.js +5 -0
- package/dist/core/deploy/plan.js +696 -0
- package/dist/core/deploy/reload-apply.js +361 -0
- package/dist/core/deploy/reload.js +358 -0
- package/dist/core/deploy/sources.js +169 -0
- package/dist/core/deploy/types.js +1 -0
- package/dist/core/epic.js +54 -0
- package/dist/core/fsutil.js +191 -0
- package/dist/core/home.js +70 -0
- package/dist/core/knowledge/facts.js +229 -0
- package/dist/core/knowledge/index.js +4 -0
- package/dist/core/knowledge/search.js +112 -0
- package/dist/core/knowledge/store.js +123 -0
- package/dist/core/knowledge/types.js +1 -0
- package/dist/core/ledger/check.js +42 -0
- package/dist/core/ledger/hooks.js +80 -0
- package/dist/core/ledger/index.js +7 -0
- package/dist/core/ledger/query.js +17 -0
- package/dist/core/ledger/record.js +84 -0
- package/dist/core/ledger/status.js +53 -0
- package/dist/core/ledger/store.js +98 -0
- package/dist/core/ledger/types.js +108 -0
- package/dist/core/logs/index.js +4 -0
- package/dist/core/logs/registry.js +50 -0
- package/dist/core/logs/scan.js +195 -0
- package/dist/core/logs/triage.js +160 -0
- package/dist/core/logs/types.js +1 -0
- package/dist/core/lsx.js +10 -0
- package/dist/core/lz4.js +66 -0
- package/dist/core/mods.js +229 -0
- package/dist/core/process.js +49 -0
- package/dist/core/project/build-steps.js +83 -0
- package/dist/core/project/describe.js +74 -0
- package/dist/core/project/index.js +5 -0
- package/dist/core/project/load.js +146 -0
- package/dist/core/project/schema.js +264 -0
- package/dist/core/project/types.js +3 -0
- package/dist/core/prompts.js +72 -0
- package/dist/core/registry.js +35 -0
- package/dist/core/safety/backup.js +206 -0
- package/dist/core/safety/execute.js +188 -0
- package/dist/core/safety/index.js +5 -0
- package/dist/core/safety/plan.js +122 -0
- package/dist/core/safety/restore.js +172 -0
- package/dist/core/safety/types.js +1 -0
- package/dist/core/scaffold/bootstrap.js +7 -0
- package/dist/core/scaffold/catalog.js +28 -0
- package/dist/core/scaffold/claude-assets.js +271 -0
- package/dist/core/scaffold/collisions.js +64 -0
- package/dist/core/scaffold/emit.js +356 -0
- package/dist/core/scaffold/engine.js +680 -0
- package/dist/core/scaffold/ids.js +78 -0
- package/dist/core/scaffold/index.js +10 -0
- package/dist/core/scaffold/jsonpath.js +121 -0
- package/dist/core/scaffold/load.js +81 -0
- package/dist/core/scaffold/render.js +85 -0
- package/dist/core/scaffold/sequence.js +396 -0
- package/dist/core/scaffold/types.js +309 -0
- package/dist/core/steam.js +63 -0
- package/dist/core/surfaceutil.js +36 -0
- package/dist/core/testplan/check.js +67 -0
- package/dist/core/testplan/index.js +5 -0
- package/dist/core/testplan/load.js +100 -0
- package/dist/core/testplan/registry.js +317 -0
- package/dist/core/testplan/run.js +389 -0
- package/dist/core/testplan/types.js +82 -0
- package/dist/core/text/converters/bg3.js +50 -0
- package/dist/core/text/converters/cyberpunk.js +36 -0
- package/dist/core/text/converters/index.js +261 -0
- package/dist/core/text/diff/cr2w.js +180 -0
- package/dist/core/text/diff/index.js +16 -0
- package/dist/core/text/diff/loca.js +43 -0
- package/dist/core/text/diff/lsx.js +126 -0
- package/dist/core/text/handles.js +86 -0
- package/dist/core/text/index.js +6 -0
- package/dist/core/text/json.js +46 -0
- package/dist/core/text/templates/cache.js +71 -0
- package/dist/core/text/templates/index.js +3 -0
- package/dist/core/text/templates/library.js +245 -0
- package/dist/core/text/templates/types.js +51 -0
- package/dist/core/text/toolchain.js +16 -0
- package/dist/core/text/types.js +1 -0
- package/dist/core/text/validate.js +149 -0
- package/dist/core/text/xml.js +58 -0
- package/dist/core/toolchain/datafetch.js +151 -0
- package/dist/core/toolchain/env.js +63 -0
- package/dist/core/toolchain/features.js +170 -0
- package/dist/core/toolchain/index.js +8 -0
- package/dist/core/toolchain/install.js +191 -0
- package/dist/core/toolchain/locate.js +275 -0
- package/dist/core/toolchain/remember.js +84 -0
- package/dist/core/toolchain/run.js +318 -0
- package/dist/core/toolchain/specs.js +351 -0
- package/dist/core/toolchain/types.js +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/userconfig.js +66 -0
- package/dist/core/validate/context.js +139 -0
- package/dist/core/validate/index.js +5 -0
- package/dist/core/validate/registry.js +58 -0
- package/dist/core/validate/run.js +131 -0
- package/dist/core/validate/suppressions.js +91 -0
- package/dist/core/validate/types.js +2 -0
- package/dist/core/yamledit.js +51 -0
- package/dist/index.js +389 -0
- package/dist/server.js +1914 -0
- package/dist/surfaces/baldursgate3/compat.js +164 -0
- package/dist/surfaces/baldursgate3/identity.js +110 -0
- package/dist/surfaces/baldursgate3/index/build.js +331 -0
- package/dist/surfaces/baldursgate3/index/index.js +17 -0
- package/dist/surfaces/baldursgate3/index/info.js +40 -0
- package/dist/surfaces/baldursgate3/index/parsers/loca.js +41 -0
- package/dist/surfaces/baldursgate3/index/parsers/lsx.js +137 -0
- package/dist/surfaces/baldursgate3/index/parsers/stats.js +45 -0
- package/dist/surfaces/baldursgate3/index/parsers/treasuretable.js +57 -0
- package/dist/surfaces/baldursgate3/index/parsers/xmlutil.js +142 -0
- package/dist/surfaces/baldursgate3/index/queries.js +327 -0
- package/dist/surfaces/baldursgate3/index/schema.js +62 -0
- package/dist/surfaces/baldursgate3/index.js +3 -0
- package/dist/surfaces/baldursgate3/logs.js +278 -0
- package/dist/surfaces/baldursgate3/modsettings.js +115 -0
- package/dist/surfaces/baldursgate3/pak.js +153 -0
- package/dist/surfaces/baldursgate3/settings.js +49 -0
- package/dist/surfaces/baldursgate3/surface.js +232 -0
- package/dist/surfaces/baldursgate3/validators/bridge.js +7 -0
- package/dist/surfaces/baldursgate3/validators/files.js +476 -0
- package/dist/surfaces/baldursgate3/validators/items.js +1001 -0
- package/dist/surfaces/baldursgate3/validators/progression.js +734 -0
- package/dist/surfaces/baldursgate3/validators/stats.js +1076 -0
- package/dist/surfaces/baldursgate3/validators/story.js +790 -0
- package/dist/surfaces/baldursgate3/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/compat.js +223 -0
- package/dist/surfaces/cyberpunk2077/index/build.js +231 -0
- package/dist/surfaces/cyberpunk2077/index/index.js +17 -0
- package/dist/surfaces/cyberpunk2077/index/info.js +51 -0
- package/dist/surfaces/cyberpunk2077/index/model.js +52 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweak.js +483 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +183 -0
- package/dist/surfaces/cyberpunk2077/index/queries.js +504 -0
- package/dist/surfaces/cyberpunk2077/index/resource.js +135 -0
- package/dist/surfaces/cyberpunk2077/index/schema.js +90 -0
- package/dist/surfaces/cyberpunk2077/index.js +3 -0
- package/dist/surfaces/cyberpunk2077/logs.js +742 -0
- package/dist/surfaces/cyberpunk2077/paths.js +12 -0
- package/dist/surfaces/cyberpunk2077/surface.js +218 -0
- package/dist/surfaces/cyberpunk2077/validators/bridge.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/cr2w.js +349 -0
- package/dist/surfaces/cyberpunk2077/validators/garments.js +1181 -0
- package/dist/surfaces/cyberpunk2077/validators/mesh.js +338 -0
- package/dist/surfaces/cyberpunk2077/validators/modsettings.js +120 -0
- package/dist/surfaces/cyberpunk2077/validators/packaging.js +463 -0
- package/dist/surfaces/cyberpunk2077/validators/red4ext.js +318 -0
- package/dist/surfaces/cyberpunk2077/validators/redscript.js +193 -0
- package/dist/surfaces/cyberpunk2077/validators/templates.js +7 -0
- package/dist/surfaces/cyberpunk2077/validators/tweaks.js +768 -0
- package/dist/surfaces/cyberpunk2077/validators/vehicles.js +531 -0
- package/dist/surfaces/cyberpunk2077/validators/xl.js +308 -0
- package/dist/surfaces/eldenring/binders.js +111 -0
- package/dist/surfaces/eldenring/compat.js +166 -0
- package/dist/surfaces/eldenring/dcx.js +49 -0
- package/dist/surfaces/eldenring/index.js +13 -0
- package/dist/surfaces/eldenring/loaders.js +132 -0
- package/dist/surfaces/eldenring/logs.js +67 -0
- package/dist/surfaces/eldenring/paramdef.js +256 -0
- package/dist/surfaces/eldenring/params.js +129 -0
- package/dist/surfaces/eldenring/surface.js +121 -0
- package/dist/surfaces/eldenring/tools.js +52 -0
- package/dist/surfaces/eldenring/validators/binders.js +193 -0
- package/dist/surfaces/eldenring/validators/packages.js +159 -0
- package/dist/surfaces/eldenring/validators/params.js +334 -0
- package/dist/surfaces/eldenring/validators/profiles.js +287 -0
- package/dist/surfaces/index.js +34 -0
- package/dist/surfaces/nivalisnights/compat.js +229 -0
- package/dist/surfaces/nivalisnights/index/build.js +310 -0
- package/dist/surfaces/nivalisnights/index/index.js +5 -0
- package/dist/surfaces/nivalisnights/index/queries.js +231 -0
- package/dist/surfaces/nivalisnights/index.js +4 -0
- package/dist/surfaces/nivalisnights/logs.js +473 -0
- package/dist/surfaces/nivalisnights/surface.js +195 -0
- package/dist/surfaces/skyrimse/archives.js +229 -0
- package/dist/surfaces/skyrimse/compat.js +163 -0
- package/dist/surfaces/skyrimse/index.js +12 -0
- package/dist/surfaces/skyrimse/loadorder.js +174 -0
- package/dist/surfaces/skyrimse/logs.js +241 -0
- package/dist/surfaces/skyrimse/plugins.js +149 -0
- package/dist/surfaces/skyrimse/records.js +94 -0
- package/dist/surfaces/skyrimse/surface.js +240 -0
- package/dist/surfaces/skyrimse/validators/archive-records.js +165 -0
- package/dist/surfaces/skyrimse/validators/archives.js +110 -0
- package/dist/surfaces/skyrimse/validators/plugins.js +374 -0
- package/dist/surfaces/skyrimse/validators/records.js +117 -0
- package/dist/surfaces/stardewvalley/compat.js +190 -0
- package/dist/surfaces/stardewvalley/config.js +42 -0
- package/dist/surfaces/stardewvalley/contentpatcher.js +203 -0
- package/dist/surfaces/stardewvalley/index/index.js +229 -0
- package/dist/surfaces/stardewvalley/index/wiki.js +190 -0
- package/dist/surfaces/stardewvalley/index.js +12 -0
- package/dist/surfaces/stardewvalley/logs.js +307 -0
- package/dist/surfaces/stardewvalley/mods.js +182 -0
- package/dist/surfaces/stardewvalley/surface.js +244 -0
- package/dist/surfaces/stardewvalley/validators/content.js +321 -0
- package/dist/surfaces/stardewvalley/validators/fields.js +147 -0
- package/dist/surfaces/stardewvalley/validators/loadorder.js +69 -0
- package/dist/surfaces/stardewvalley/validators/manifest.js +221 -0
- package/dist/surfaces/subnautica2/compat.js +138 -0
- package/dist/surfaces/subnautica2/index.js +5 -0
- package/dist/surfaces/subnautica2/logs.js +129 -0
- package/dist/surfaces/subnautica2/paks.js +173 -0
- package/dist/surfaces/subnautica2/surface.js +151 -0
- package/dist/surfaces/subnautica2/validators/packaging.js +298 -0
- package/dist/surfaces/valheim/compat.js +171 -0
- package/dist/surfaces/valheim/index/index.js +301 -0
- package/dist/surfaces/valheim/index/tables.js +103 -0
- package/dist/surfaces/valheim/index.js +11 -0
- package/dist/surfaces/valheim/logs.js +197 -0
- package/dist/surfaces/valheim/packaging.js +131 -0
- package/dist/surfaces/valheim/plugins.js +270 -0
- package/dist/surfaces/valheim/references.js +121 -0
- package/dist/surfaces/valheim/surface.js +223 -0
- package/dist/surfaces/valheim/validators/packaging.js +218 -0
- package/dist/surfaces/valheim/validators/plugins.js +287 -0
- package/dist/surfaces/valheim/validators/references.js +169 -0
- package/knowledge/baldursgate3/animations.gr2.yaml +166 -0
- package/knowledge/baldursgate3/audio.voice.yaml +182 -0
- package/knowledge/baldursgate3/audio.wwise.yaml +104 -0
- package/knowledge/baldursgate3/charactercreation.heads-hair.yaml +193 -0
- package/knowledge/baldursgate3/communitylibrary.shared-content.yaml +129 -0
- package/knowledge/baldursgate3/compatibilityframework.api.yaml +176 -0
- package/knowledge/baldursgate3/crash.causes.yaml +90 -0
- package/knowledge/baldursgate3/dialogue.dialogbank.yaml +120 -0
- package/knowledge/baldursgate3/dialogue.files.yaml +236 -0
- package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +386 -0
- package/knowledge/baldursgate3/dialogue.yaml +122 -0
- package/knowledge/baldursgate3/feats.classes.yaml +164 -0
- package/knowledge/baldursgate3/items.cloth-physics.yaml +167 -0
- package/knowledge/baldursgate3/items.roottemplates.yaml +320 -0
- package/knowledge/baldursgate3/items.visualbank.yaml +628 -0
- package/knowledge/baldursgate3/journal.quests.yaml +214 -0
- package/knowledge/baldursgate3/loadorder.modsettings.yaml +128 -0
- package/knowledge/baldursgate3/localization.handles.yaml +92 -0
- package/knowledge/baldursgate3/misc.encoding-and-practice.yaml +111 -0
- package/knowledge/baldursgate3/origins.companions.yaml +144 -0
- package/knowledge/baldursgate3/osiris.signatures.yaml +211 -0
- package/knowledge/baldursgate3/packaging.meta.yaml +155 -0
- package/knowledge/baldursgate3/packaging.paks.yaml +79 -0
- package/knowledge/baldursgate3/process.launch.yaml +34 -0
- package/knowledge/baldursgate3/progressions.yaml +92 -0
- package/knowledge/baldursgate3/publishing.modio-nexus.yaml +170 -0
- package/knowledge/baldursgate3/publishing.toolkit-modio.yaml +135 -0
- package/knowledge/baldursgate3/races.progression.yaml +228 -0
- package/knowledge/baldursgate3/scriptextender.console.yaml +257 -0
- package/knowledge/baldursgate3/scriptextender.yaml +252 -0
- package/knowledge/baldursgate3/stats.boosts.yaml +293 -0
- package/knowledge/baldursgate3/stats.equipment-kits.yaml +69 -0
- package/knowledge/baldursgate3/stats.interrupts.yaml +45 -0
- package/knowledge/baldursgate3/stats.itemcombos.yaml +65 -0
- package/knowledge/baldursgate3/stats.spells.yaml +293 -0
- package/knowledge/baldursgate3/stats.weapons.yaml +210 -0
- package/knowledge/baldursgate3/toolchain.divine.yaml +179 -0
- package/knowledge/baldursgate3/toolchain.lslib-formats.yaml +242 -0
- package/knowledge/baldursgate3/toolkit.levels.yaml +177 -0
- package/knowledge/baldursgate3/ui.icons-atlas.yaml +78 -0
- package/knowledge/baldursgate3/ui.icons.yaml +412 -0
- package/knowledge/baldursgate3/ui.mcm.yaml +161 -0
- package/knowledge/baldursgate3/vfx.materials.yaml +174 -0
- package/knowledge/baldursgate3/visuals.characterfix.yaml +73 -0
- package/knowledge/cyberpunk2077/animations.anims.yaml +118 -0
- package/knowledge/cyberpunk2077/anims.archivexl.yaml +163 -0
- package/knowledge/cyberpunk2077/anims.authoring.yaml +579 -0
- package/knowledge/cyberpunk2077/anims.encoder.yaml +156 -0
- package/knowledge/cyberpunk2077/anims.graph.yaml +629 -0
- package/knowledge/cyberpunk2077/anims.method.yaml +360 -0
- package/knowledge/cyberpunk2077/anims.pipeline.yaml +352 -0
- package/knowledge/cyberpunk2077/anims.roundtrip.yaml +222 -0
- package/knowledge/cyberpunk2077/anims.sets.yaml +176 -0
- package/knowledge/cyberpunk2077/archivexl.bodytypes.yaml +149 -0
- package/knowledge/cyberpunk2077/archivexl.dynamic-appearances.yaml +218 -0
- package/knowledge/cyberpunk2077/archivexl.manifest.yaml +214 -0
- package/knowledge/cyberpunk2077/audio.events.yaml +168 -0
- package/knowledge/cyberpunk2077/audio.sounds.yaml +164 -0
- package/knowledge/cyberpunk2077/audio.voicesets.yaml +146 -0
- package/knowledge/cyberpunk2077/cet.sandbox.yaml +219 -0
- package/knowledge/cyberpunk2077/clothing.garments.yaml +283 -0
- package/knowledge/cyberpunk2077/clothing.refits.yaml +215 -0
- package/knowledge/cyberpunk2077/codeware.overview.yaml +111 -0
- package/knowledge/cyberpunk2077/codeware.systems.yaml +192 -0
- package/knowledge/cyberpunk2077/drones.combat.yaml +416 -0
- package/knowledge/cyberpunk2077/drones.control.yaml +552 -0
- package/knowledge/cyberpunk2077/entities.appearance.yaml +166 -0
- package/knowledge/cyberpunk2077/equipmentex.slots.yaml +151 -0
- package/knowledge/cyberpunk2077/fx.effects.yaml +246 -0
- package/knowledge/cyberpunk2077/fx.lights.yaml +171 -0
- package/knowledge/cyberpunk2077/hair.modding.yaml +145 -0
- package/knowledge/cyberpunk2077/items.wiring.yaml +246 -0
- package/knowledge/cyberpunk2077/loadorder.archives.yaml +71 -0
- package/knowledge/cyberpunk2077/mesh.blender.yaml +209 -0
- package/knowledge/cyberpunk2077/nativedb.dump.yaml +98 -0
- package/knowledge/cyberpunk2077/npc.appearance.yaml +275 -0
- package/knowledge/cyberpunk2077/npc.behaviour.yaml +128 -0
- package/knowledge/cyberpunk2077/npc.locomotion.yaml +490 -0
- package/knowledge/cyberpunk2077/packaging.deploy.yaml +175 -0
- package/knowledge/cyberpunk2077/packaging.variants.yaml +176 -0
- package/knowledge/cyberpunk2077/player.control.yaml +918 -0
- package/knowledge/cyberpunk2077/process.assetbuild.yaml +154 -0
- package/knowledge/cyberpunk2077/process.lessons.yaml +276 -0
- package/knowledge/cyberpunk2077/quests.minor.yaml +46 -0
- package/knowledge/cyberpunk2077/quests.scenes.yaml +282 -0
- package/knowledge/cyberpunk2077/quickhacks.tiers.yaml +41 -0
- package/knowledge/cyberpunk2077/red4ext.api.yaml +645 -0
- package/knowledge/cyberpunk2077/red4ext.plugins.yaml +124 -0
- package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +245 -0
- package/knowledge/cyberpunk2077/redscript.compile-log.yaml +76 -0
- package/knowledge/cyberpunk2077/redscript.modules.yaml +118 -0
- package/knowledge/cyberpunk2077/toolchain.wolvenkit.yaml +163 -0
- package/knowledge/cyberpunk2077/tweakdb.loot-recipes.yaml +184 -0
- package/knowledge/cyberpunk2077/tweakdb.melee.yaml +352 -0
- package/knowledge/cyberpunk2077/tweakdb.weapons.yaml +325 -0
- package/knowledge/cyberpunk2077/tweakxl.hotreload.yaml +151 -0
- package/knowledge/cyberpunk2077/tweakxl.yaml-syntax.yaml +208 -0
- package/knowledge/cyberpunk2077/ui.icons.yaml +259 -0
- package/knowledge/cyberpunk2077/ui.inkatlas.yaml +81 -0
- package/knowledge/cyberpunk2077/ui.modsettings.yaml +204 -0
- package/knowledge/cyberpunk2077/vehicles.control.yaml +475 -0
- package/knowledge/cyberpunk2077/vehicles.dashboard-ui.yaml +203 -0
- package/knowledge/cyberpunk2077/vehicles.entities.yaml +195 -0
- package/knowledge/cyberpunk2077/vehicles.wiring.yaml +137 -0
- package/knowledge/cyberpunk2077/wolvenkit.cli.yaml +167 -0
- package/knowledge/cyberpunk2077/workspot.system.yaml +55 -0
- package/knowledge/cyberpunk2077/world.interactions.yaml +41 -0
- package/knowledge/cyberpunk2077/world.loot-container.yaml +167 -0
- package/knowledge/cyberpunk2077/world.sectors.yaml +233 -0
- package/knowledge/eldenring/formats.binders.yaml +237 -0
- package/knowledge/eldenring/launcher.me3.yaml +230 -0
- package/knowledge/eldenring/launcher.modengine2.yaml +119 -0
- package/knowledge/eldenring/params.paramdef.yaml +149 -0
- package/knowledge/eldenring/params.regulation.yaml +142 -0
- package/knowledge/eldenring/saves.online.yaml +118 -0
- package/knowledge/eldenring/toolchain.cli.yaml +256 -0
- package/knowledge/nivalisnights/modding.runtime.yaml +157 -0
- package/knowledge/skyrimse/archives.bsa.yaml +212 -0
- package/knowledge/skyrimse/install.detection.yaml +41 -0
- package/knowledge/skyrimse/loadorder.files.yaml +242 -0
- package/knowledge/skyrimse/plugins.esl.yaml +189 -0
- package/knowledge/skyrimse/plugins.format.yaml +224 -0
- package/knowledge/skyrimse/skse.runtime.yaml +283 -0
- package/knowledge/skyrimse/toolchain.xedit-spriggit.yaml +336 -0
- package/knowledge/stardewvalley/contentpatcher.format.yaml +338 -0
- package/knowledge/stardewvalley/distribution.channels.yaml +236 -0
- package/knowledge/stardewvalley/game.versions.yaml +202 -0
- package/knowledge/stardewvalley/install.layout.yaml +277 -0
- package/knowledge/stardewvalley/smapi.loader.yaml +284 -0
- package/knowledge/stardewvalley/smapi.logs.yaml +329 -0
- package/knowledge/stardewvalley/smapi.manifest.yaml +293 -0
- package/knowledge/stardewvalley/toolchain.build.yaml +261 -0
- package/knowledge/stardewvalley/wiki.dataformat.yaml +130 -0
- package/knowledge/subnautica2/game.build.yaml +331 -0
- package/knowledge/subnautica2/modding.state.yaml +210 -0
- package/knowledge/subnautica2/ue5.containers.yaml +72 -0
- package/knowledge/subnautica2/ue5.paks.yaml +203 -0
- package/knowledge/subnautica2/ue5.tooling.yaml +175 -0
- package/knowledge/subnautica2/ue5.ue4ss.yaml +203 -0
- package/knowledge/valheim/bepinex.loader.yaml +247 -0
- package/knowledge/valheim/bepinex.plugins.yaml +251 -0
- package/knowledge/valheim/game.versions.yaml +321 -0
- package/knowledge/valheim/install.layout.yaml +216 -0
- package/knowledge/valheim/jotunn.library.yaml +334 -0
- package/knowledge/valheim/thunderstore.packaging.yaml +217 -0
- package/knowledge/valheim/toolchain.build.yaml +111 -0
- package/package.json +29 -0
- package/scaffolds/baldursgate3/new-class-injection.yaml +148 -0
- package/scaffolds/baldursgate3/new-item.yaml +264 -0
- package/scaffolds/baldursgate3/new-mod.yaml +174 -0
- package/scaffolds/baldursgate3/new-passive.yaml +100 -0
- package/scaffolds/baldursgate3/new-project.yaml +118 -0
- package/scaffolds/baldursgate3/new-quest-stub.yaml +159 -0
- package/scaffolds/baldursgate3/new-spell.yaml +102 -0
- package/scaffolds/baldursgate3/new-status.yaml +119 -0
- package/scaffolds/baldursgate3/sequences/subclass-kit.yaml +50 -0
- package/scaffolds/cyberpunk2077/edit-tweak.yaml +70 -0
- package/scaffolds/cyberpunk2077/mesh-export-preset.yaml +170 -0
- package/scaffolds/cyberpunk2077/new-entity-patch.yaml +255 -0
- package/scaffolds/cyberpunk2077/new-garment-refit.yaml +279 -0
- package/scaffolds/cyberpunk2077/new-item-chain.yaml +548 -0
- package/scaffolds/cyberpunk2077/new-localization.yaml +130 -0
- package/scaffolds/cyberpunk2077/new-mod-settings.yaml +133 -0
- package/scaffolds/cyberpunk2077/new-player-replacer.yaml +1003 -0
- package/scaffolds/cyberpunk2077/new-project.yaml +157 -0
- package/scaffolds/cyberpunk2077/new-sector-node.yaml +156 -0
- package/scaffolds/cyberpunk2077/new-tweak.yaml +121 -0
- package/scaffolds/cyberpunk2077/new-vehicle-livery.yaml +148 -0
- package/scaffolds/cyberpunk2077/new-workspot-entity.yaml +177 -0
- package/scaffolds/cyberpunk2077/sequences/iconic-weapon.yaml +72 -0
- package/scaffolds/cyberpunk2077/sequences/quickhack-takeover.yaml +213 -0
- package/scaffolds/stardewvalley/new-cp-patch.yaml +106 -0
- package/scaffolds/stardewvalley/new-csharp-mod.yaml +195 -0
- package/scaffolds/stardewvalley/new-project.yaml +184 -0
- package/scaffolds/stardewvalley/sequences/content-pack.yaml +31 -0
- package/scripts/mw-tool.mjs +184 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { bridgeDoneSchema, bridgeHeartbeatSchema, bridgeResultSchema } from "./types.js";
|
|
4
|
+
import { HEARTBEAT_FRESH_MS } from "./locate.js";
|
|
5
|
+
import { bridgeFileNames, cetModDir } from "./paths.js";
|
|
6
|
+
import { pollBudgetMs, POLL_INTERVAL_MS, readCompleteResult } from "./poll.js";
|
|
7
|
+
export const CET_RESULT_TAG = "[MODWRIGHT-RESULT]";
|
|
8
|
+
export const CET_DONE_TAG = "[MODWRIGHT-DONE]";
|
|
9
|
+
export const CET_HEARTBEAT_TAG = "[MODWRIGHT-HEARTBEAT]";
|
|
10
|
+
export const CET_LOG_HEARTBEAT_FRESH_MS = 45_000;
|
|
11
|
+
export const CET_LOG_TAIL_BYTES = 512 * 1024;
|
|
12
|
+
const CET_LINE_TIMESTAMP = /\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) UTC([+-]\d{2}:\d{2})\]/;
|
|
13
|
+
function cetPluginDir(install, surface) {
|
|
14
|
+
const cet = surface?.logs?.(install).find((source) => source.id === "cet");
|
|
15
|
+
if (cet?.path)
|
|
16
|
+
return path.dirname(cet.path);
|
|
17
|
+
return path.join(install.root, "bin", "x64", "plugins", "cyber_engine_tweaks");
|
|
18
|
+
}
|
|
19
|
+
export const CET_MOD_LOG_FALLBACK = "ModWrightBridge.log";
|
|
20
|
+
export async function cetLogCandidates(install, surface) {
|
|
21
|
+
const dir = cetPluginDir(install, surface);
|
|
22
|
+
const modDir = cetModDir(install);
|
|
23
|
+
return [
|
|
24
|
+
...(await modLogsIn(modDir)),
|
|
25
|
+
path.join(dir, "cyber_engine_tweaks.log"),
|
|
26
|
+
path.join(dir, "gamelog.log"),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
async function modLogsIn(modDir) {
|
|
30
|
+
let names;
|
|
31
|
+
try {
|
|
32
|
+
names = await fs.readdir(modDir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [path.join(modDir, CET_MOD_LOG_FALLBACK)];
|
|
36
|
+
}
|
|
37
|
+
const logs = [];
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
if (!name.toLowerCase().endsWith(".log"))
|
|
40
|
+
continue;
|
|
41
|
+
const file = path.join(modDir, name);
|
|
42
|
+
try {
|
|
43
|
+
logs.push({ file, mtimeMs: (await fs.stat(file)).mtimeMs });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (logs.length === 0)
|
|
50
|
+
return [path.join(modDir, CET_MOD_LOG_FALLBACK)];
|
|
51
|
+
return logs.sort((a, b) => b.mtimeMs - a.mtimeMs).map((l) => l.file);
|
|
52
|
+
}
|
|
53
|
+
export async function readLogTail(absPath, maxBytes = CET_LOG_TAIL_BYTES) {
|
|
54
|
+
let handle;
|
|
55
|
+
try {
|
|
56
|
+
handle = await fs.open(absPath, "r");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const { size } = await handle.stat();
|
|
63
|
+
const start = Math.max(0, size - maxBytes);
|
|
64
|
+
const length = size - start;
|
|
65
|
+
const buffer = Buffer.alloc(length);
|
|
66
|
+
if (length > 0)
|
|
67
|
+
await handle.read(buffer, 0, length, start);
|
|
68
|
+
const text = buffer.toString("utf8");
|
|
69
|
+
return start > 0 ? text.slice(text.indexOf("\n") + 1) : text;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await handle.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseLineTimestamp(line) {
|
|
76
|
+
const match = line.match(CET_LINE_TIMESTAMP);
|
|
77
|
+
if (!match)
|
|
78
|
+
return undefined;
|
|
79
|
+
const parsed = new Date(`${match[1]}T${match[2]}${match[3]}`);
|
|
80
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
81
|
+
}
|
|
82
|
+
const TAGS = [
|
|
83
|
+
{ tag: "result", text: CET_RESULT_TAG },
|
|
84
|
+
{ tag: "done", text: CET_DONE_TAG },
|
|
85
|
+
{ tag: "heartbeat", text: CET_HEARTBEAT_TAG },
|
|
86
|
+
];
|
|
87
|
+
export function scanCetLog(text, file) {
|
|
88
|
+
const entries = [];
|
|
89
|
+
const lines = text.split(/\r?\n/);
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
let best;
|
|
92
|
+
for (const candidate of TAGS) {
|
|
93
|
+
const at = line.indexOf(`${candidate.text} `);
|
|
94
|
+
if (at >= 0 && (best === undefined || at < best.at))
|
|
95
|
+
best = { tag: candidate.tag, at, text: candidate.text };
|
|
96
|
+
}
|
|
97
|
+
if (!best)
|
|
98
|
+
continue;
|
|
99
|
+
const json = line.slice(best.at + best.text.length + 1);
|
|
100
|
+
if (json === "")
|
|
101
|
+
continue;
|
|
102
|
+
const at = parseLineTimestamp(line.slice(0, best.at));
|
|
103
|
+
entries.push({
|
|
104
|
+
tag: best.tag,
|
|
105
|
+
json,
|
|
106
|
+
bytes: Buffer.byteLength(json, "utf8"),
|
|
107
|
+
...(at ? { at } : {}),
|
|
108
|
+
file,
|
|
109
|
+
index: entries.length,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
function olderThanArm(entry, armedAt) {
|
|
115
|
+
if (!armedAt || !entry.at)
|
|
116
|
+
return false;
|
|
117
|
+
return entry.at.getTime() < armedAt.getTime() - 1000;
|
|
118
|
+
}
|
|
119
|
+
export function cetCapabilities(heartbeat) {
|
|
120
|
+
for (const entry of heartbeat.writeRootErrors ?? []) {
|
|
121
|
+
const at = entry.indexOf("capabilities: ");
|
|
122
|
+
if (at < 0)
|
|
123
|
+
continue;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(entry.slice(at + "capabilities: ".length));
|
|
126
|
+
if (parsed && typeof parsed === "object")
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
export function cetSandboxIo(heartbeat) {
|
|
136
|
+
const caps = cetCapabilities(heartbeat);
|
|
137
|
+
const io = caps?.["io"];
|
|
138
|
+
if (typeof io === "boolean")
|
|
139
|
+
return io;
|
|
140
|
+
if (heartbeat.writeRoot && heartbeat.writeRoot !== "log-only")
|
|
141
|
+
return true;
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export async function findCetResult(files, match, deps = {}) {
|
|
145
|
+
const readTail = deps.readTail ?? ((absPath) => readLogTail(absPath));
|
|
146
|
+
let incomplete;
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const text = await readTail(file);
|
|
149
|
+
if (text === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const entries = scanCetLog(text, file);
|
|
152
|
+
const dones = entries.filter((e) => e.tag === "done");
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.tag !== "result" || olderThanArm(entry, match.armedAt))
|
|
155
|
+
continue;
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = bridgeResultSchema.safeParse(JSON.parse(entry.json));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
parsed = undefined;
|
|
162
|
+
}
|
|
163
|
+
const done = dones.find((d) => {
|
|
164
|
+
if (d.index <= entry.index)
|
|
165
|
+
return false;
|
|
166
|
+
try {
|
|
167
|
+
const marker = bridgeDoneSchema.parse(JSON.parse(d.json));
|
|
168
|
+
return marker.id === match.id && marker.nonce === match.nonce;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
if (!done) {
|
|
175
|
+
if (parsed?.success && parsed.data.id === match.id && parsed.data.nonce === match.nonce) {
|
|
176
|
+
incomplete ??= { status: "incomplete", reason: "no-done-marker", file, actualBytes: entry.bytes };
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const marker = bridgeDoneSchema.parse(JSON.parse(done.json));
|
|
181
|
+
if (marker.bytes !== entry.bytes) {
|
|
182
|
+
incomplete = { status: "incomplete", reason: "truncated", file, expectedBytes: marker.bytes, actualBytes: entry.bytes };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!parsed?.success) {
|
|
186
|
+
incomplete = { status: "incomplete", reason: "truncated", file, expectedBytes: marker.bytes, actualBytes: entry.bytes };
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (parsed.data.id !== match.id || parsed.data.nonce !== match.nonce)
|
|
190
|
+
continue;
|
|
191
|
+
return { status: "complete", result: parsed.data, file };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return incomplete ?? { status: "absent" };
|
|
195
|
+
}
|
|
196
|
+
export async function readCetHeartbeat(files, deps = {}) {
|
|
197
|
+
const readTail = deps.readTail ?? ((absPath) => readLogTail(absPath));
|
|
198
|
+
let newest;
|
|
199
|
+
let newestAt = Number.NEGATIVE_INFINITY;
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
const text = await readTail(file);
|
|
202
|
+
if (text === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
for (const entry of scanCetLog(text, file)) {
|
|
205
|
+
if (entry.tag !== "heartbeat")
|
|
206
|
+
continue;
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = bridgeHeartbeatSchema.safeParse(JSON.parse(entry.json));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!parsed.success)
|
|
215
|
+
continue;
|
|
216
|
+
const at = Date.parse(parsed.data.at);
|
|
217
|
+
if (Number.isNaN(at) || at < newestAt)
|
|
218
|
+
continue;
|
|
219
|
+
newestAt = at;
|
|
220
|
+
newest = { heartbeat: parsed.data, file, raw: entry.json, ...(entry.at ? { loggedAt: entry.at } : {}) };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return newest;
|
|
224
|
+
}
|
|
225
|
+
function defaultReadFile(absPath) {
|
|
226
|
+
return fs.readFile(absPath, "utf8");
|
|
227
|
+
}
|
|
228
|
+
export async function pollCyberpunk(input) {
|
|
229
|
+
const now = input.now ?? (() => new Date());
|
|
230
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
231
|
+
const readTail = input.readTail ?? ((absPath) => readLogTail(absPath));
|
|
232
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
233
|
+
const budgetMs = Math.min(input.timeoutMs ?? Number.POSITIVE_INFINITY, pollBudgetMs(input.probeCount, input.windowSeconds));
|
|
234
|
+
const start = now();
|
|
235
|
+
let lastIncomplete;
|
|
236
|
+
for (;;) {
|
|
237
|
+
for (const dir of input.dirs ?? []) {
|
|
238
|
+
const result = await readCompleteResult(dir, input.id, "cyberpunk2077", readFile);
|
|
239
|
+
if (result && result.nonce === input.nonce) {
|
|
240
|
+
return { status: "complete", result, transport: "file", file: dir, elapsedMs: now().getTime() - start.getTime() };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const scan = await findCetResult(input.files, { id: input.id, nonce: input.nonce, armedAt: input.armedAt }, { readTail });
|
|
244
|
+
if (scan.status === "complete") {
|
|
245
|
+
return { status: "complete", result: scan.result, transport: "log", file: scan.file, elapsedMs: now().getTime() - start.getTime() };
|
|
246
|
+
}
|
|
247
|
+
if (scan.status === "incomplete")
|
|
248
|
+
lastIncomplete = scan;
|
|
249
|
+
const elapsedMs = now().getTime() - start.getTime();
|
|
250
|
+
if (elapsedMs >= budgetMs) {
|
|
251
|
+
if (lastIncomplete?.reason === "truncated") {
|
|
252
|
+
return {
|
|
253
|
+
status: "timeout",
|
|
254
|
+
reason: "truncated",
|
|
255
|
+
file: lastIncomplete.file,
|
|
256
|
+
elapsedMs,
|
|
257
|
+
message: `A ${CET_RESULT_TAG} line for "${input.id}" is in ${lastIncomplete.file}, but its ${CET_DONE_TAG} marker declares ` +
|
|
258
|
+
`${lastIncomplete.expectedBytes} bytes and the line carries ${lastIncomplete.actualBytes} — the line was cut mid-write. ` +
|
|
259
|
+
`Re-run the console line rather than reading a partial answer.`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return classifyCetTimeout(input, elapsedMs, lastIncomplete, { readTail, readFile, now });
|
|
263
|
+
}
|
|
264
|
+
await sleep(POLL_INTERVAL_MS);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export function pollCetLog(input) {
|
|
268
|
+
return pollCyberpunk(input);
|
|
269
|
+
}
|
|
270
|
+
async function readCetFileHeartbeat(dirs, readFile) {
|
|
271
|
+
let newest;
|
|
272
|
+
let newestAt = Number.NEGATIVE_INFINITY;
|
|
273
|
+
for (const dir of dirs) {
|
|
274
|
+
let raw;
|
|
275
|
+
try {
|
|
276
|
+
raw = await readFile(path.join(dir, bridgeFileNames("cyberpunk2077").heartbeat));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
let parsed;
|
|
282
|
+
try {
|
|
283
|
+
parsed = bridgeHeartbeatSchema.safeParse(JSON.parse(raw));
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (!parsed.success)
|
|
289
|
+
continue;
|
|
290
|
+
const at = Date.parse(parsed.data.at);
|
|
291
|
+
if (Number.isNaN(at) || at < newestAt)
|
|
292
|
+
continue;
|
|
293
|
+
newestAt = at;
|
|
294
|
+
newest = { heartbeat: parsed.data, dir };
|
|
295
|
+
}
|
|
296
|
+
return newest;
|
|
297
|
+
}
|
|
298
|
+
async function classifyCetTimeout(input, elapsedMs, lastIncomplete, deps) {
|
|
299
|
+
const fromFile = await readCetFileHeartbeat(input.dirs ?? [], deps.readFile);
|
|
300
|
+
const fromLog = await readCetHeartbeat(input.files, { readTail: deps.readTail });
|
|
301
|
+
const useFile = fromFile !== undefined &&
|
|
302
|
+
(fromLog === undefined || Date.parse(fromFile.heartbeat.at) >= Date.parse(fromLog.heartbeat.at));
|
|
303
|
+
const found = useFile
|
|
304
|
+
? { heartbeat: fromFile.heartbeat, transport: "file", path: fromFile.dir, freshMs: HEARTBEAT_FRESH_MS }
|
|
305
|
+
: fromLog
|
|
306
|
+
? { heartbeat: fromLog.heartbeat, transport: "log", path: fromLog.file, freshMs: CET_LOG_HEARTBEAT_FRESH_MS }
|
|
307
|
+
: undefined;
|
|
308
|
+
if (!found) {
|
|
309
|
+
return {
|
|
310
|
+
status: "timeout",
|
|
311
|
+
reason: "absent",
|
|
312
|
+
elapsedMs,
|
|
313
|
+
message: `No heartbeat anywhere: no ${bridgeFileNames("cyberpunk2077").heartbeat} in ${(input.dirs ?? []).join(" or ") || "any candidate directory"}, ` +
|
|
314
|
+
`and no ${CET_HEARTBEAT_TAG} line in ${input.files.join(" or ")} — the bridge is not deployed, the game has not run ` +
|
|
315
|
+
`since deploy, or its output lands somewhere none of those cover (the bridge's own m.where() line tells those apart).`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const heartbeatFrom = { transport: found.transport, path: found.path };
|
|
319
|
+
const ageMs = deps.now().getTime() - Date.parse(found.heartbeat.at);
|
|
320
|
+
if (ageMs > found.freshMs) {
|
|
321
|
+
return {
|
|
322
|
+
status: "timeout",
|
|
323
|
+
reason: "stale",
|
|
324
|
+
heartbeat: found.heartbeat,
|
|
325
|
+
heartbeatFrom,
|
|
326
|
+
elapsedMs,
|
|
327
|
+
message: `The newest heartbeat (${found.transport} transport, ${found.path}) is ${Math.round(ageMs / 1000)}s old — the game is not running, or the bridge stopped ticking.`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (found.heartbeat.state === "running") {
|
|
331
|
+
return {
|
|
332
|
+
status: "timeout",
|
|
333
|
+
reason: "hung",
|
|
334
|
+
heartbeat: found.heartbeat,
|
|
335
|
+
heartbeatFrom,
|
|
336
|
+
elapsedMs,
|
|
337
|
+
message: `The bridge heartbeat is fresh and still "running" after the poll budget — a probe is hung, or its settleSeconds has not elapsed.`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (found.heartbeat.lastRequestId !== input.id) {
|
|
341
|
+
return {
|
|
342
|
+
status: "timeout",
|
|
343
|
+
reason: "unseen",
|
|
344
|
+
heartbeat: found.heartbeat,
|
|
345
|
+
heartbeatFrom,
|
|
346
|
+
elapsedMs,
|
|
347
|
+
message: `The bridge is idle and its lastRequestId ("${found.heartbeat.lastRequestId ?? "none"}") is not "${input.id}" — it never saw ` +
|
|
348
|
+
`this request. On the log transport that means the console line was never run (or m.arm refused it, which prints as an ` +
|
|
349
|
+
`"[ModWrightBridge] arm refused: …" line); on the file transport it means the request went to the wrong write root.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
status: "timeout",
|
|
354
|
+
reason: "hung",
|
|
355
|
+
heartbeat: found.heartbeat,
|
|
356
|
+
heartbeatFrom,
|
|
357
|
+
...(lastIncomplete ? { file: lastIncomplete.file } : {}),
|
|
358
|
+
elapsedMs,
|
|
359
|
+
message: `The bridge is idle and reports lastRequestId "${input.id}" but no complete result reached either transport. ` +
|
|
360
|
+
`Check the CET log for an error while handling the request.`,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { bridgeFileNames, bridgeCandidateDirs, cetModDir } from "./paths.js";
|
|
3
|
+
export { HEARTBEAT_FRESH_MS, locateBridge } from "./locate.js";
|
|
4
|
+
export { bridgeStatus } from "./status.js";
|
|
5
|
+
export { armBridge, buildRequest, generateNonce, generateRequestId, planArmRequest } from "./arm.js";
|
|
6
|
+
export { CET_CONSOLE_MOD, cetArmConsoleLine, cetArmSteps, compactRequestJson, luaSingleQuoted } from "./cet-console.js";
|
|
7
|
+
export { CET_DONE_TAG, CET_HEARTBEAT_TAG, CET_LOG_HEARTBEAT_FRESH_MS, CET_LOG_TAIL_BYTES, CET_RESULT_TAG, CET_MOD_LOG_FALLBACK, cetCapabilities, cetLogCandidates, cetSandboxIo, findCetResult, pollCetLog, pollCyberpunk, readCetHeartbeat, readLogTail, scanCetLog, } from "./cet-log.js";
|
|
8
|
+
export { buildPendingArm, listPendingArms, pendingArmDir, pendingArmFile, pendingArmSchema, planRecordPendingArm, readPendingArm } from "./pending.js";
|
|
9
|
+
export { MAX_BUDGET_SECONDS, MIN_BUDGET_SECONDS, POLL_INTERVAL_MS, SECONDS_PER_PROBE, pollBudgetMs, pollForResult, readCompleteResult } from "./poll.js";
|
|
10
|
+
export { bridgeDeployTarget, bridgeId, bridgesRootDir, bridgeSourceDir, planStageBridge, stageBridge, stagedBridgeDir } from "./stage.js";
|
|
11
|
+
export { bridgeNeverShipValidator } from "./validate.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { bridgeCandidateDirs, bridgeFileNames } from "./paths.js";
|
|
4
|
+
import { bridgeHeartbeatSchema } from "./types.js";
|
|
5
|
+
export const HEARTBEAT_FRESH_MS = 15_000;
|
|
6
|
+
function defaultReadFile(absPath) {
|
|
7
|
+
return fs.readFile(absPath, "utf8");
|
|
8
|
+
}
|
|
9
|
+
export async function locateBridge(game, install, surface, deps = {}) {
|
|
10
|
+
const candidates = bridgeCandidateDirs(game, install, surface);
|
|
11
|
+
const files = bridgeFileNames(game);
|
|
12
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
13
|
+
const now = deps.now ?? (() => new Date());
|
|
14
|
+
for (const dir of candidates) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(path.join(dir, files.heartbeat));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = bridgeHeartbeatSchema.safeParse(JSON.parse(raw));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!parsed.success)
|
|
30
|
+
continue;
|
|
31
|
+
const ageMs = now().getTime() - Date.parse(parsed.data.at);
|
|
32
|
+
return { candidates, dir, heartbeat: parsed.data, ageMs, raw };
|
|
33
|
+
}
|
|
34
|
+
return { candidates };
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
const BG3_FILES = {
|
|
3
|
+
heartbeat: "ModWright_hb.json",
|
|
4
|
+
heartbeatClient: "ModWright_hb_client.json",
|
|
5
|
+
request: "ModWright_req.json",
|
|
6
|
+
requestMarker: "ModWright_req.marker.json",
|
|
7
|
+
result: (id) => `ModWright_res_${id}.json`,
|
|
8
|
+
resultDone: (id) => `ModWright_res_${id}.done.json`,
|
|
9
|
+
log: "ModWright_log.json",
|
|
10
|
+
};
|
|
11
|
+
const CP2077_FILES = {
|
|
12
|
+
heartbeat: "modwright_hb.json",
|
|
13
|
+
request: "modwright_req.json",
|
|
14
|
+
requestMarker: "modwright_req.marker.json",
|
|
15
|
+
result: (id) => `modwright_res_${id}.json`,
|
|
16
|
+
resultDone: (id) => `modwright_res_${id}.done.json`,
|
|
17
|
+
};
|
|
18
|
+
export function bridgeFileNames(game) {
|
|
19
|
+
return game === "baldursgate3" ? BG3_FILES : CP2077_FILES;
|
|
20
|
+
}
|
|
21
|
+
export function bridgeCandidateDirs(game, install, surface) {
|
|
22
|
+
if (game === "baldursgate3") {
|
|
23
|
+
const root = surface.modRoots(install).find((r) => r.id === "scriptextender");
|
|
24
|
+
if (!root) {
|
|
25
|
+
throw new Error(`bridge: surface "${surface.id}" declares no "scriptextender" mod root — cannot resolve the BG3 bridge directory.`);
|
|
26
|
+
}
|
|
27
|
+
return [root.path];
|
|
28
|
+
}
|
|
29
|
+
const binX64 = path.join(install.root, "bin", "x64");
|
|
30
|
+
return [cetModDir(install), binX64, install.root];
|
|
31
|
+
}
|
|
32
|
+
export function cetModDir(install) {
|
|
33
|
+
return path.join(install.root, "bin", "x64", "plugins", "cyber_engine_tweaks", "mods", "ModWrightBridge");
|
|
34
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { planDelete, planFile } from "../safety/index.js";
|
|
5
|
+
import { PROTOCOL_VERSION, bridgeGameSchema, bridgeRequestSchema } from "./types.js";
|
|
6
|
+
export function pendingArmDir(projectRoot) {
|
|
7
|
+
return path.join(projectRoot, ".modwright", "bridge", "pending");
|
|
8
|
+
}
|
|
9
|
+
export function pendingArmFile(projectRoot, id) {
|
|
10
|
+
return path.join(pendingArmDir(projectRoot), `${id}.json`);
|
|
11
|
+
}
|
|
12
|
+
export const pendingArmSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
protocolVersion: z.number().int(),
|
|
15
|
+
game: bridgeGameSchema,
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
nonce: z.string().min(1),
|
|
18
|
+
probeCount: z.number().int().positive(),
|
|
19
|
+
armedAt: z.string(),
|
|
20
|
+
armedUntil: z.string(),
|
|
21
|
+
windowSeconds: z.number().nonnegative().optional(),
|
|
22
|
+
consoleLine: z.string().min(1).optional(),
|
|
23
|
+
request: bridgeRequestSchema,
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
export function buildPendingArm(request, consoleLine, armedAt, windowSeconds) {
|
|
27
|
+
return pendingArmSchema.parse({
|
|
28
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
29
|
+
game: request.game,
|
|
30
|
+
id: request.id,
|
|
31
|
+
nonce: request.nonce,
|
|
32
|
+
probeCount: request.probes.length,
|
|
33
|
+
armedAt: armedAt.toISOString(),
|
|
34
|
+
armedUntil: request.armedUntil,
|
|
35
|
+
...(windowSeconds !== undefined ? { windowSeconds } : {}),
|
|
36
|
+
...(consoleLine !== undefined ? { consoleLine } : {}),
|
|
37
|
+
request,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function planRecordPendingArm(projectRoot, pending) {
|
|
41
|
+
return {
|
|
42
|
+
purpose: `record pending console arm ${pending.id} (${pending.game})`,
|
|
43
|
+
operations: [planFile(pendingArmFile(projectRoot, pending.id), `${JSON.stringify(pending, null, 2)}\n`)],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function readPendingArm(projectRoot, id) {
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await fs.readFile(pendingArmFile(projectRoot, id), "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const parsed = pendingArmSchema.safeParse(JSON.parse(raw));
|
|
55
|
+
return parsed.success ? parsed.data : undefined;
|
|
56
|
+
}
|
|
57
|
+
export async function listPendingArms(projectRoot, game, opts) {
|
|
58
|
+
let names;
|
|
59
|
+
try {
|
|
60
|
+
names = await fs.readdir(pendingArmDir(projectRoot));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const activeAtMs = opts?.activeAt ? opts.activeAt.getTime() : undefined;
|
|
66
|
+
const found = [];
|
|
67
|
+
for (const name of names) {
|
|
68
|
+
if (!name.endsWith(".json"))
|
|
69
|
+
continue;
|
|
70
|
+
const pending = await readPendingArm(projectRoot, name.slice(0, -".json".length));
|
|
71
|
+
if (!pending)
|
|
72
|
+
continue;
|
|
73
|
+
if (game && pending.game !== game)
|
|
74
|
+
continue;
|
|
75
|
+
if (activeAtMs !== undefined && Date.parse(pending.armedUntil) <= activeAtMs)
|
|
76
|
+
continue;
|
|
77
|
+
found.push(pending);
|
|
78
|
+
}
|
|
79
|
+
return found.sort((a, b) => Date.parse(b.armedAt) - Date.parse(a.armedAt));
|
|
80
|
+
}
|
|
81
|
+
export async function planPruneExpiredPendingArms(projectRoot, game, before) {
|
|
82
|
+
const beforeMs = before.getTime();
|
|
83
|
+
const ops = [];
|
|
84
|
+
for (const pending of await listPendingArms(projectRoot, game)) {
|
|
85
|
+
if (Date.parse(pending.armedUntil) <= beforeMs)
|
|
86
|
+
ops.push(planDelete(pendingArmFile(projectRoot, pending.id)));
|
|
87
|
+
}
|
|
88
|
+
return ops;
|
|
89
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { HEARTBEAT_FRESH_MS } from "./locate.js";
|
|
4
|
+
import { bridgeFileNames } from "./paths.js";
|
|
5
|
+
import { bridgeDoneSchema, bridgeHeartbeatSchema, bridgeResultSchema } from "./types.js";
|
|
6
|
+
export const POLL_INTERVAL_MS = 500;
|
|
7
|
+
export const MIN_BUDGET_SECONDS = 15;
|
|
8
|
+
export const SECONDS_PER_PROBE = 3;
|
|
9
|
+
export const MAX_BUDGET_SECONDS = 300;
|
|
10
|
+
export function pollBudgetMs(probeCount, windowSeconds = 0) {
|
|
11
|
+
const seconds = Math.min(MAX_BUDGET_SECONDS, Math.max(MIN_BUDGET_SECONDS, SECONDS_PER_PROBE * probeCount) + windowSeconds);
|
|
12
|
+
return seconds * 1000;
|
|
13
|
+
}
|
|
14
|
+
function defaultReadFile(absPath) {
|
|
15
|
+
return fs.readFile(absPath, "utf8");
|
|
16
|
+
}
|
|
17
|
+
function realSleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
export async function readCompleteResult(dir, id, game, readFile) {
|
|
21
|
+
const files = bridgeFileNames(game);
|
|
22
|
+
let doneRaw;
|
|
23
|
+
try {
|
|
24
|
+
doneRaw = await readFile(path.join(dir, files.resultDone(id)));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
let done;
|
|
30
|
+
try {
|
|
31
|
+
done = bridgeDoneSchema.parse(JSON.parse(doneRaw));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (done.id !== id)
|
|
37
|
+
return undefined;
|
|
38
|
+
let resultRaw;
|
|
39
|
+
try {
|
|
40
|
+
resultRaw = await readFile(path.join(dir, files.result(id)));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (Buffer.byteLength(resultRaw, "utf8") !== done.bytes)
|
|
46
|
+
return undefined;
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = bridgeResultSchema.safeParse(JSON.parse(resultRaw));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (!parsed.success)
|
|
55
|
+
return undefined;
|
|
56
|
+
return parsed.data;
|
|
57
|
+
}
|
|
58
|
+
async function lastProbeStarted(dir, id, game, readFile) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await readFile(path.join(dir, bridgeFileNames(game).result(id)));
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
const probes = Array.isArray(parsed.probes) ? parsed.probes : [];
|
|
63
|
+
const last = probes[probes.length - 1];
|
|
64
|
+
return typeof last?.id === "string" ? last.id : undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function classifyTimeout(input, readFile, now, elapsedMs) {
|
|
71
|
+
for (const dir of input.dirs) {
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = await readFile(path.join(dir, bridgeFileNames(input.game).heartbeat));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let heartbeat;
|
|
80
|
+
try {
|
|
81
|
+
heartbeat = bridgeHeartbeatSchema.parse(JSON.parse(raw));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const ageMs = now().getTime() - Date.parse(heartbeat.at);
|
|
87
|
+
if (ageMs > HEARTBEAT_FRESH_MS) {
|
|
88
|
+
return {
|
|
89
|
+
status: "timeout",
|
|
90
|
+
reason: "stale",
|
|
91
|
+
heartbeat,
|
|
92
|
+
heartbeatDir: dir,
|
|
93
|
+
elapsedMs,
|
|
94
|
+
message: `Heartbeat at ${dir} is ${Math.round(ageMs / 1000)}s old — the game is not running, or the bridge crashed. Check the SE/CET log.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (heartbeat.state === "running") {
|
|
98
|
+
const lastProbe = await lastProbeStarted(dir, input.id, input.game, readFile);
|
|
99
|
+
return {
|
|
100
|
+
status: "timeout",
|
|
101
|
+
reason: "hung",
|
|
102
|
+
heartbeat,
|
|
103
|
+
heartbeatDir: dir,
|
|
104
|
+
elapsedMs,
|
|
105
|
+
message: `Bridge heartbeat is fresh and still "running" after the poll budget — a probe is hung.` +
|
|
106
|
+
(lastProbe ? ` Last probe started: ${lastProbe}.` : ""),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (heartbeat.lastRequestId !== input.id) {
|
|
110
|
+
return {
|
|
111
|
+
status: "timeout",
|
|
112
|
+
reason: "unseen",
|
|
113
|
+
heartbeat,
|
|
114
|
+
heartbeatDir: dir,
|
|
115
|
+
elapsedMs,
|
|
116
|
+
message: `Bridge is idle and its lastRequestId ("${heartbeat.lastRequestId ?? "none"}") does not match the armed ` +
|
|
117
|
+
`request ("${input.id}") — the request was never seen, so the write root (CET) or storage dir is wrong.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
status: "timeout",
|
|
122
|
+
reason: "hung",
|
|
123
|
+
heartbeat,
|
|
124
|
+
heartbeatDir: dir,
|
|
125
|
+
elapsedMs,
|
|
126
|
+
message: `Bridge is idle and reports lastRequestId "${input.id}" but produced no result — check the SE/CET log for an error while handling the request.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
status: "timeout",
|
|
131
|
+
reason: "absent",
|
|
132
|
+
elapsedMs,
|
|
133
|
+
message: "No heartbeat found in any candidate directory — the bridge is not deployed, or the game has not run since deploy.",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export async function pollForResult(input) {
|
|
137
|
+
const now = input.now ?? (() => new Date());
|
|
138
|
+
const sleep = input.sleep ?? realSleep;
|
|
139
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
140
|
+
const budgetMs = Math.min(input.timeoutMs ?? Number.POSITIVE_INFINITY, pollBudgetMs(input.probeCount, input.windowSeconds));
|
|
141
|
+
const start = now();
|
|
142
|
+
for (;;) {
|
|
143
|
+
for (const dir of input.dirs) {
|
|
144
|
+
const result = await readCompleteResult(dir, input.id, input.game, readFile);
|
|
145
|
+
if (result && result.nonce === input.nonce) {
|
|
146
|
+
return { status: "complete", result, dir, elapsedMs: now().getTime() - start.getTime() };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const elapsedMs = now().getTime() - start.getTime();
|
|
150
|
+
if (elapsedMs >= budgetMs) {
|
|
151
|
+
return classifyTimeout({ game: input.game, dirs: input.dirs, id: input.id }, readFile, now, elapsedMs);
|
|
152
|
+
}
|
|
153
|
+
await sleep(POLL_INTERVAL_MS);
|
|
154
|
+
}
|
|
155
|
+
}
|