esp-modkit 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE +21 -0
- data/README.md +117 -0
- data/docs/architecture.md +125 -0
- data/docs/authoring-guide.md +206 -0
- data/docs/getting-started.md +183 -0
- data/docs/reference/api/active-project.md +22 -0
- data/docs/reference/api/agent.md +24 -0
- data/docs/reference/api/docs-generator.md +20 -0
- data/docs/reference/api/http-server.md +46 -0
- data/docs/reference/api/index.md +38 -0
- data/docs/reference/api/introspection.md +17 -0
- data/docs/reference/api/mcp-installer.md +26 -0
- data/docs/reference/api/mcp-server.md +27 -0
- data/docs/reference/api/mw-builder.md +14 -0
- data/docs/reference/api/mw-data-files.md +20 -0
- data/docs/reference/api/mw-dialogue-dsl.md +58 -0
- data/docs/reference/api/mw-i18n.md +20 -0
- data/docs/reference/api/mw-linter.md +18 -0
- data/docs/reference/api/mw-loader.md +26 -0
- data/docs/reference/api/mw-openmw-config.md +15 -0
- data/docs/reference/api/mw-operations.md +24 -0
- data/docs/reference/api/mw-preflight.md +17 -0
- data/docs/reference/api/mw-reference-index.md +21 -0
- data/docs/reference/api/mw-scaffolder.md +13 -0
- data/docs/reference/api/mw-script-blob.md +31 -0
- data/docs/reference/api/mw-script-extractor.md +17 -0
- data/docs/reference/api/operations.md +25 -0
- data/docs/reference/api/plugins.md +24 -0
- data/docs/reference/api/preferences.md +13 -0
- data/docs/reference/api/project-marker.md +23 -0
- data/docs/reference/api/providers.md +22 -0
- data/docs/reference/api/recents.md +17 -0
- data/docs/reference/api/ui.md +21 -0
- data/docs/reference/api/vcs.md +17 -0
- data/docs/reference/api/watcher.md +11 -0
- data/docs/reference/commands.md +271 -0
- data/docs/walkthrough.md +193 -0
- data/exe/esp +10 -0
- data/lib/esp/active_project.rb +71 -0
- data/lib/esp/agent.rb +104 -0
- data/lib/esp/cli/docs.rb +44 -0
- data/lib/esp/cli/i18n.rb +67 -0
- data/lib/esp/cli/mcp.rb +52 -0
- data/lib/esp/cli/plugins.rb +42 -0
- data/lib/esp/cli/refs.rb +137 -0
- data/lib/esp/cli/support.rb +87 -0
- data/lib/esp/cli.rb +317 -0
- data/lib/esp/docs_generator.rb +148 -0
- data/lib/esp/http_server.rb +232 -0
- data/lib/esp/introspection.rb +151 -0
- data/lib/esp/mcp_installer.rb +122 -0
- data/lib/esp/mcp_server.rb +465 -0
- data/lib/esp/mw/builder.rb +71 -0
- data/lib/esp/mw/data_files.rb +67 -0
- data/lib/esp/mw/dialogue_dsl.rb +209 -0
- data/lib/esp/mw/i18n.rb +113 -0
- data/lib/esp/mw/linter.rb +103 -0
- data/lib/esp/mw/loader.rb +130 -0
- data/lib/esp/mw/openmw_config.rb +138 -0
- data/lib/esp/mw/operations.rb +374 -0
- data/lib/esp/mw/preflight.rb +161 -0
- data/lib/esp/mw/reference_index.rb +182 -0
- data/lib/esp/mw/scaffolder.rb +197 -0
- data/lib/esp/mw/script_blob.rb +87 -0
- data/lib/esp/mw/script_extractor.rb +85 -0
- data/lib/esp/mw/tes3conv.rb +38 -0
- data/lib/esp/operations.rb +285 -0
- data/lib/esp/plugins.rb +75 -0
- data/lib/esp/preferences.rb +63 -0
- data/lib/esp/project_marker.rb +99 -0
- data/lib/esp/providers/anthropic.rb +74 -0
- data/lib/esp/providers/ollama.rb +102 -0
- data/lib/esp/providers/openai.rb +91 -0
- data/lib/esp/providers.rb +76 -0
- data/lib/esp/recents.rb +74 -0
- data/lib/esp/ui.rb +144 -0
- data/lib/esp/vcs.rb +112 -0
- data/lib/esp/version.rb +11 -0
- data/lib/esp/watcher.rb +55 -0
- data/lib/esp.rb +85 -0
- data/locales/en.yml +164 -0
- data/locales/fr.yml +10 -0
- metadata +241 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a70a9625b2c9f54539ee7e5a561275fd9020e90996112022a40406484278805
|
|
4
|
+
data.tar.gz: 8d9342e2c02d123e7247e81198a8d01e46785b94871685716bb7898c97a76a02
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1bde927727d1388c8c41cebff7640739cfb807809e68768c33982823b982b6da0fc05832c0dd3cf289a18317f2199295dc6a8e76175246a189543636a5b966a7
|
|
7
|
+
data.tar.gz: 30c973f51eaa4693f6cb12cfecff8517a30bb58c92c49862d26a3d7dd6da6b2c200b03e6d2d870c8b77c925e64d15cd106a93d5b48496b44c738721b31f50c7e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `esp-modkit` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project aims to
|
|
5
|
+
adhere to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Packaged as the `esp-modkit` gem.** `gem install esp-modkit` puts the
|
|
11
|
+
`esp` command on PATH — no toolchain clone required. The gem ships the
|
|
12
|
+
library, locale catalogues, generated reference docs, and the narrative
|
|
13
|
+
guides. (step 24, Half A)
|
|
14
|
+
- `esp doctor` — reports Ruby/gem version, whether `tes3conv` is on PATH,
|
|
15
|
+
and whether the vanilla references index exists, so a fresh install can
|
|
16
|
+
self-check its prerequisites.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- The gem's executable (`exe/esp`) is plain Ruby that loads via RubyGems;
|
|
20
|
+
the repo-local `bin/esp` bash launcher (rbenv + Bundler pinning) stays for
|
|
21
|
+
in-repo development only.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
- `bin/mw` — the renamed-to-`esp` deprecation shim from step 22.5. Anyone
|
|
25
|
+
still invoking `mw` must switch to `esp`. AI clients with a stale
|
|
26
|
+
`.mcp.json` should re-run `esp mcp install`.
|
|
27
|
+
|
|
28
|
+
### Prerequisites
|
|
29
|
+
- **tes3conv is not bundled** (it is an external Rust binary, not on
|
|
30
|
+
Homebrew). Install it from
|
|
31
|
+
https://github.com/Greatness7/tes3conv/releases or via
|
|
32
|
+
`cargo install --git https://github.com/Greatness7/tes3conv`, then run
|
|
33
|
+
`esp doctor` to confirm. The Homebrew tap installs it for you.
|
|
34
|
+
- **Native extensions** (`sqlite3`, `zstd-ruby`) compile on install — a C
|
|
35
|
+
toolchain is required.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corey Ellis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# esp — author Morrowind plugins as source code
|
|
2
|
+
|
|
3
|
+
`esp` is a Ruby toolchain for authoring Morrowind plugins **without the
|
|
4
|
+
Construction Set**. You write records in **JSON, Ruby, Python,
|
|
5
|
+
JavaScript, or TypeScript**; `esp` builds them to an `.esp` via
|
|
6
|
+
[tes3conv], manages scripts, translations, and dialogue, and lints
|
|
7
|
+
against the real vanilla game data. Every operation renders as human
|
|
8
|
+
text or structured JSON, so a CLI, an HTTP API, an MCP server, and an AI
|
|
9
|
+
agent all drive the same pipeline.
|
|
10
|
+
|
|
11
|
+
## Why — source is truth, the `.esp` is output
|
|
12
|
+
|
|
13
|
+
The Construction Set is a direct-manipulation binary editor: the `.esp`
|
|
14
|
+
*is* the document. `esp` inverts that. **You author intent as
|
|
15
|
+
human-readable source and the `.esp` is a disposable build artifact** —
|
|
16
|
+
the relationship a compiler has with its object files. You never
|
|
17
|
+
hand-edit the binary; you edit source and rebuild.
|
|
18
|
+
|
|
19
|
+
That one inversion buys the whole software-engineering toolchain:
|
|
20
|
+
`git diff` / blame / branch / review, procedural generation, lint as a
|
|
21
|
+
build step, CI, HTTP/MCP automation, and AI authoring. The cost is the
|
|
22
|
+
loss of direct manipulation — spatial placement and visual browsing.
|
|
23
|
+
[**ESPresso**](docs/architecture.md) is the planned AI-first GUI built on
|
|
24
|
+
this backend, where you describe intent, an agent writes diffable
|
|
25
|
+
source, and you review the git diff.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
Install once:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
gem install esp-modkit # puts `esp` on your PATH
|
|
33
|
+
# tes3conv is a required prerequisite, not on Homebrew: download a release
|
|
34
|
+
# https://github.com/Greatness7/tes3conv/releases
|
|
35
|
+
# or: cargo install --git https://github.com/Greatness7/tes3conv
|
|
36
|
+
esp doctor # confirm Ruby, tes3conv, and the references index
|
|
37
|
+
esp refs unpack # unpack vanilla ESMs to ~/.config/esp/references/ (one-time)
|
|
38
|
+
esp refs index # build the SQLite index there (one-time)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Native extensions (`sqlite3`, `zstd-ruby`) compile on install, so a C
|
|
42
|
+
toolchain is needed. To work from a checkout instead of the gem, clone the
|
|
43
|
+
repo, `bundle install`, and put `bin/esp` on your `PATH` (the launcher pins
|
|
44
|
+
this repo's Ruby + Gemfile).
|
|
45
|
+
|
|
46
|
+
Then per-project — anywhere on disk:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
mkdir -p ~/morrowind/MyFirstMod && cd ~/morrowind/MyFirstMod
|
|
50
|
+
esp init # writes .esp/project.json + git init + mods/ + dist/
|
|
51
|
+
esp new MyMod # scaffold mods/MyMod/
|
|
52
|
+
esp lint MyMod # check references against vanilla data
|
|
53
|
+
esp build MyMod # -> dist/MyMod.esp (in *this* project)
|
|
54
|
+
esp install MyMod # OpenMW: register via data= + content=
|
|
55
|
+
esp install MyMod --to-data-files # Original engine: copy into Morrowind/Data Files/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`esp` walks up from the current directory to find the `.esp/project.json`
|
|
59
|
+
marker, so commands work from any subdir of your project tree —
|
|
60
|
+
no flags needed inside a project, `--root PATH` if you're scripting
|
|
61
|
+
across several, or `export ESP_PROJECT_ROOT=…` to pin one per shell.
|
|
62
|
+
|
|
63
|
+
**New here? Read the [walkthrough](docs/walkthrough.md)** — one plugin
|
|
64
|
+
from empty to built, with the thesis shown in practice.
|
|
65
|
+
|
|
66
|
+
## Three frontends, one pipeline
|
|
67
|
+
|
|
68
|
+
Every command has a `--json` mode whose payload is identical across all
|
|
69
|
+
three frontends:
|
|
70
|
+
|
|
71
|
+
| Frontend | Command | For |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| CLI | `esp <cmd>` | humans + scripts |
|
|
74
|
+
| HTTP | `esp serve` | ESPresso, local automation |
|
|
75
|
+
| MCP | `esp mcp serve` (`esp mcp install` to wire up) | AI clients (Claude Code / Desktop) |
|
|
76
|
+
|
|
77
|
+
## Command surface
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
doctor check prerequisites (Ruby, tes3conv, refs index)
|
|
81
|
+
init [NAME] new project (marker + git init + mods/ + dist/)
|
|
82
|
+
new MOD [--format rb|py|js|ts] scaffold a mod
|
|
83
|
+
build [MOD] [--locale fr] [--all] [--install] build -> dist/MOD[.locale].esp
|
|
84
|
+
watch MOD rebuild on change
|
|
85
|
+
lint MOD dangling-ref check (needs refs index)
|
|
86
|
+
extract-scripts MOD hoist inline script text to files
|
|
87
|
+
unpack PLUGIN [NAME] .esp -> mods/NAME/NAME.json
|
|
88
|
+
install MOD [--copy-to PATH | --to-data-files] OpenMW + optional file copy
|
|
89
|
+
refs unpack | index | find Q vanilla data: build + query
|
|
90
|
+
i18n check MOD missing/orphan locale keys
|
|
91
|
+
docs build | introspect regenerate / dump docs
|
|
92
|
+
serve | mcp serve HTTP API / MCP server
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
- **[Walkthrough](docs/walkthrough.md)** — empty → built, end to end.
|
|
98
|
+
- **[Getting started](docs/getting-started.md)** — install, paths on
|
|
99
|
+
macOS, wiring `esp` into an AI client.
|
|
100
|
+
- **[Authoring guide](docs/authoring-guide.md)** — source formats,
|
|
101
|
+
`text_source`, dialogue DSL, translation, linting.
|
|
102
|
+
- **[Architecture](docs/architecture.md)** — layered design, the
|
|
103
|
+
roadmap, why each piece exists.
|
|
104
|
+
- **[Command reference](docs/reference/commands.md)** *(auto-generated)*
|
|
105
|
+
- **[API reference](docs/reference/api/index.md)** *(auto-generated)*
|
|
106
|
+
|
|
107
|
+
Regenerate the auto pages with `bin/esp docs build`; a pre-commit hook
|
|
108
|
+
fails if they drift.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
bundle exec rake # RuboCop + Minitest (must be green)
|
|
114
|
+
bundle exec rake admin # local quality reports (coverage, lint, stats)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
[tes3conv]: https://github.com/Greatness7/tes3conv
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Layered design
|
|
4
|
+
|
|
5
|
+
```mermaid
|
|
6
|
+
flowchart TB
|
|
7
|
+
subgraph frontends["Frontends"]
|
|
8
|
+
direction LR
|
|
9
|
+
CLI(CLI)
|
|
10
|
+
GUI(GUI — planned)
|
|
11
|
+
MCP(MCP — planned)
|
|
12
|
+
HTTP(HTTP — planned)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
subgraph service["Service layer (returns structured results)"]
|
|
16
|
+
direction LR
|
|
17
|
+
Builder
|
|
18
|
+
Linter
|
|
19
|
+
ScriptExtractor
|
|
20
|
+
ReferenceIndex
|
|
21
|
+
I18n
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subgraph core["Core primitives"]
|
|
25
|
+
direction LR
|
|
26
|
+
Loader
|
|
27
|
+
Preflight
|
|
28
|
+
ScriptBlob
|
|
29
|
+
Tes3conv
|
|
30
|
+
OpenmwConfig
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph protocol["Source-program protocol"]
|
|
34
|
+
direction LR
|
|
35
|
+
InProcess["Ruby (in-process)"]
|
|
36
|
+
Subprocess["Spawn (Python / JS / TS)"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
frontends --> service
|
|
40
|
+
service --> core
|
|
41
|
+
core --> protocol
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Today **CLI**, **HTTP** (`esp serve`), and **MCP** (`esp mcp serve`) are
|
|
45
|
+
all live. The HTTP layer is a thin WEBrick wrapper over the same
|
|
46
|
+
service-layer modules — every endpoint payload matches the
|
|
47
|
+
corresponding `esp <cmd> --json` shape. The MCP server exposes the same
|
|
48
|
+
operations as tools over stdio JSON-RPC for AI clients. The **GUI**
|
|
49
|
+
(ESPresso, a separate repo) talks to `esp serve` instead of spawning a CLI
|
|
50
|
+
per call — including `POST /agent`, which streams a Claude tool-use loop
|
|
51
|
+
(`Esp::Agent`) over Server-Sent Events, dispatching the same tool surface
|
|
52
|
+
MCP exposes.
|
|
53
|
+
|
|
54
|
+
## Why JSON is the universal contract
|
|
55
|
+
|
|
56
|
+
The build pipeline:
|
|
57
|
+
|
|
58
|
+
```mermaid
|
|
59
|
+
flowchart LR
|
|
60
|
+
Src["source file<br/>(.json / .rb / .py / .js / .ts)"]
|
|
61
|
+
--> Loader
|
|
62
|
+
--> Records["records<br/>(Array<Hash>)"]
|
|
63
|
+
--> I18n["I18n.resolve!<br/>(@t: sentinels)"]
|
|
64
|
+
--> Preflight
|
|
65
|
+
--> Tmp[JSON tempfile]
|
|
66
|
+
--> tes3conv
|
|
67
|
+
--> Esp["dist/<Mod>.esp"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Anywhere along that chain you can substitute a different producer.
|
|
71
|
+
Today's authoring formats — JSON, Ruby, Python, JavaScript, TypeScript
|
|
72
|
+
— all converge on the same `Array<Hash>` shape. A future language
|
|
73
|
+
(Lua? Rust?) gets added by registering an interpreter command in
|
|
74
|
+
`Esp::Mw::Loader::INTERPRETERS`.
|
|
75
|
+
|
|
76
|
+
## Why two paths for i18n
|
|
77
|
+
|
|
78
|
+
`.rb` sources can call `t("key")` directly because the Ruby loader
|
|
79
|
+
runs in-process; we inject a singleton method on the eval module.
|
|
80
|
+
|
|
81
|
+
Subprocess loaders (Python/JS/TS) can't call back into Ruby. They emit
|
|
82
|
+
`"@t:key"` sentinel strings instead, which the build resolves
|
|
83
|
+
post-load via a single recursive walk. Same `Esp::Mw::I18n` instance
|
|
84
|
+
services both paths, so miss tracking is unified.
|
|
85
|
+
|
|
86
|
+
JSON has no execution model at all, so it uses sentinels too.
|
|
87
|
+
|
|
88
|
+
## Why a SQLite reference index
|
|
89
|
+
|
|
90
|
+
Vanilla data unpacks to ~200 MB of JSON across three ESMs and ~69 k
|
|
91
|
+
records. `grep` is slow and brittle (record boundaries aren't lines).
|
|
92
|
+
The index is a 4-column SQLite table built once with `esp refs index`,
|
|
93
|
+
queried in milliseconds for lookups by id / type / SQL `LIKE` pattern.
|
|
94
|
+
The linter uses it for foreign-id resolution; future tooling will too.
|
|
95
|
+
|
|
96
|
+
## Why golden tests for ZSTD blob synthesis
|
|
97
|
+
|
|
98
|
+
The Script record's variables list and bytecode placeholder are
|
|
99
|
+
embedded as ZSTD-framed binary blobs that tes3conv accepts only in a
|
|
100
|
+
very specific layout. Getting the bytes wrong is silent: builds appear
|
|
101
|
+
to succeed and OpenMW just logs warnings. The test suite sweeps every
|
|
102
|
+
vanilla Script record (~1.2 k) and asserts that our synthesis +
|
|
103
|
+
libzstd-backed decode round-trip byte-identically.
|
|
104
|
+
|
|
105
|
+
## Self-documenting outputs
|
|
106
|
+
|
|
107
|
+
Every CLI command has a `--json` mode whose payload is the same data
|
|
108
|
+
the human text branch yields. `esp docs introspect` dumps the full
|
|
109
|
+
command tree and module surface as one JSON object — that's the
|
|
110
|
+
mechanism the markdown docs in `docs/reference/` are generated from,
|
|
111
|
+
and the same mechanism the MCP server uses to register its tools.
|
|
112
|
+
|
|
113
|
+
## Roadmap status
|
|
114
|
+
|
|
115
|
+
**Phase A — the toolchain** (the "compiler") and **Phase B — production
|
|
116
|
+
toolchain + AI backbone** are done: the full authoring/build pipeline,
|
|
117
|
+
the MCP/`Operations` surface, internal tool i18n, OS-aware game-install
|
|
118
|
+
integration, and a central load manifest.
|
|
119
|
+
|
|
120
|
+
**Phase C — ESPresso** (the AI-first GUI, in the separate `espresso`
|
|
121
|
+
repo) is in
|
|
122
|
+
progress: the Tauri+SvelteKit shell and the agent workspace (chat →
|
|
123
|
+
`POST /agent` SSE → `Esp::Agent`) are built; git-diff review, visual
|
|
124
|
+
panels, and packaging (bundling a Ruby runtime) are next. The
|
|
125
|
+
local-only `.claude/ROADMAP.md` holds the authoritative step list.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Authoring guide
|
|
2
|
+
|
|
3
|
+
## Source formats
|
|
4
|
+
|
|
5
|
+
Each mod lives in `mods/<Mod>/` with exactly one source file. Pick the
|
|
6
|
+
language you're most comfortable with — they all produce the same ESP.
|
|
7
|
+
|
|
8
|
+
| Extension | Loader | Notes |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `<Mod>.json` | in-process | Array of record hashes, matches tes3conv's JSON shape |
|
|
11
|
+
| `<Mod>.rb` | in-process | Last expression must be the records array; symbol keys auto-converted |
|
|
12
|
+
| `<Mod>.py` | subprocess (`python3`) | Print a JSON array of records to stdout |
|
|
13
|
+
| `<Mod>.js` / `<Mod>.mjs` | subprocess (`node`) | Same contract |
|
|
14
|
+
| `<Mod>.ts` | subprocess (`deno run --quiet`) | Same contract |
|
|
15
|
+
|
|
16
|
+
For subprocess loaders the interpreter runs with `cwd` set to the mod's
|
|
17
|
+
source directory, so the script can read sibling files like
|
|
18
|
+
`scripts/foo.mwscript` with plain relative paths. The script's own path
|
|
19
|
+
is passed as the last command-line arg.
|
|
20
|
+
|
|
21
|
+
To add another language, add an entry to `Esp::Mw::Loader::INTERPRETERS` in
|
|
22
|
+
`lib/esp/mw/loader.rb`. The contract is just "print a JSON array of records
|
|
23
|
+
to stdout."
|
|
24
|
+
|
|
25
|
+
## Per-mod self-containment
|
|
26
|
+
|
|
27
|
+
Everything that belongs to a mod — its source file, scripts, dialogue
|
|
28
|
+
drafts, design docs, i18n catalogues — lives **inside that mod's
|
|
29
|
+
folder**. The tool repo doesn't own user content; cross-mod files
|
|
30
|
+
belong wherever the user organises them. A typical mod layout:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
mods/<Mod>/
|
|
34
|
+
├── <Mod>.rb # or <Mod>.json, <Mod>.py, etc.
|
|
35
|
+
├── design/ # docs, voice guides, narrative notes
|
|
36
|
+
├── scripts/ # .mwscript files referenced via text_source
|
|
37
|
+
├── i18n/ # locale catalogues (see Translation below)
|
|
38
|
+
└── README.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## MWScript: `text_source`
|
|
42
|
+
|
|
43
|
+
Script records carry their source code in `text`. For real scripts this
|
|
44
|
+
is verbose and unreadable inside a JSON array, so the build accepts an
|
|
45
|
+
alternative:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"type": "Script",
|
|
50
|
+
"id": "MyScript",
|
|
51
|
+
"text_source": "scripts/MyScript.mwscript"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
At build time the preflight pass reads `scripts/MyScript.mwscript`,
|
|
56
|
+
parses out variable declarations, and regenerates the SCHD header and
|
|
57
|
+
SCVR/SCDT blobs in the byte layout tes3conv expects. The `text` field
|
|
58
|
+
is set to the file's contents; `bytecode` is a placeholder (OpenMW
|
|
59
|
+
recompiles from text on load).
|
|
60
|
+
|
|
61
|
+
Going the other direction: if you `esp unpack` someone else's mod, every
|
|
62
|
+
script lives inline. Run `esp extract-scripts <Mod>` to hoist those out
|
|
63
|
+
to `scripts/<id>.mwscript` files. The transformation is lossless —
|
|
64
|
+
rebuilding produces an identical ESP.
|
|
65
|
+
|
|
66
|
+
## Dialogue (DSL, Ruby sources only)
|
|
67
|
+
|
|
68
|
+
Hand-writing DIAL and DialogueInfo records as JSON is the most painful
|
|
69
|
+
single thing the Construction Set is good at. The Ruby DSL replaces
|
|
70
|
+
the bookkeeping (per-info IDs, prev/next chains, repeated speaker
|
|
71
|
+
filters) so the source reads like the dialogue itself:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
records = [
|
|
75
|
+
{ type: 'Header', flags: '', file_type: 'Esp', author: 'me',
|
|
76
|
+
version: 1.3, num_objects: 0, masters: [] }
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
records.concat(
|
|
80
|
+
dialogue do
|
|
81
|
+
speaker 'Hrisskar Flat-Foot' do
|
|
82
|
+
topic 'Flat-Foot' do
|
|
83
|
+
info t('hrisskar.flat_foot.intro')
|
|
84
|
+
info t('hrisskar.flat_foot.legion'), pc_faction: 'Imperial Legion'
|
|
85
|
+
info t('hrisskar.flat_foot.warning'),
|
|
86
|
+
pc_faction: 'Imperial Legion',
|
|
87
|
+
pc_rank: 3,
|
|
88
|
+
result_script: 'StartScript AFSN_TrackerScript'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
topic 'AFSN_Tracker', type: :journal do
|
|
93
|
+
info 'Quest started.', journal_index: 10
|
|
94
|
+
info 'Hrisskar agreed.', journal_index: 20
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
records
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
What the DSL does for you:
|
|
103
|
+
|
|
104
|
+
- **One `Dialogue` record per `topic`**, carrying `dialogue_type`
|
|
105
|
+
(Topic / Greeting / Journal / Persuasion / Voice).
|
|
106
|
+
- **`DialogueInfo` records chained in author order** via prev_id /
|
|
107
|
+
next_id. Order is filter precedence in Morrowind — first match
|
|
108
|
+
wins, so write fallbacks last.
|
|
109
|
+
- **`speaker "Name" do … end`** scopes a speaker filter to every info
|
|
110
|
+
inside; an explicit `speaker:` on an info overrides.
|
|
111
|
+
- **Predictable IDs**: `"<topic>_<idx>"`. Git diffs stay readable.
|
|
112
|
+
- **i18n**: `t("key.path")` works inside the block — same catalogue
|
|
113
|
+
as the rest of the mod.
|
|
114
|
+
|
|
115
|
+
Supported info kwargs (all optional):
|
|
116
|
+
|
|
117
|
+
| Kwarg | Maps to |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `speaker:` | `speaker_id` |
|
|
120
|
+
| `race:` | `speaker_race` |
|
|
121
|
+
| `class:` *(or `class_:`)* | `speaker_class` |
|
|
122
|
+
| `faction:` | `speaker_faction` |
|
|
123
|
+
| `cell:` | `speaker_cell` |
|
|
124
|
+
| `sex:` | `data.speaker_sex` (`:any` / `:female` / `:male`) |
|
|
125
|
+
| `speaker_rank:` | `data.speaker_rank` |
|
|
126
|
+
| `pc_faction:` | `player_faction` |
|
|
127
|
+
| `pc_rank:` | `data.player_rank` |
|
|
128
|
+
| `disposition:` | `data.disposition` |
|
|
129
|
+
| `journal_index:` | `data.disposition` (for Journal topics) |
|
|
130
|
+
| `sound:` | `sound_path` |
|
|
131
|
+
| `result_script:` | `script_text` (inline MWScript) |
|
|
132
|
+
|
|
133
|
+
**Not yet covered** (deferred to a later step): the structured
|
|
134
|
+
`filters` array (function calls / variable comparisons / global checks),
|
|
135
|
+
`result_script_source:` (currently raises NotImplementedError — use
|
|
136
|
+
inline text for now), and the Greeting 0–9 priority cascade.
|
|
137
|
+
|
|
138
|
+
## Translation (i18n)
|
|
139
|
+
|
|
140
|
+
Strings can be authored in two complementary ways:
|
|
141
|
+
|
|
142
|
+
- **In `.rb` sources** call `t("key.path")` directly — the loader
|
|
143
|
+
exposes the helper as a singleton method on the eval module, so it
|
|
144
|
+
resolves at evaluation time.
|
|
145
|
+
- **In `.json` / `.py` / `.js` / `.ts` sources** write
|
|
146
|
+
`"@t:key.path"` sentinel strings anywhere a string value is
|
|
147
|
+
expected. The build resolves them recursively post-load.
|
|
148
|
+
|
|
149
|
+
Catalogues live at `mods/<Mod>/i18n/<locale>.yaml`. Keys are dot-pathed
|
|
150
|
+
nested-hash, with lookup falling back to the default locale (`en`) on
|
|
151
|
+
miss:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# mods/MyMod/i18n/en.yaml
|
|
155
|
+
meta:
|
|
156
|
+
description: "A friend in Seyda Neen"
|
|
157
|
+
activator:
|
|
158
|
+
stump: "Hollow Stump"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
# mods/MyMod/i18n/fr.yaml
|
|
163
|
+
meta:
|
|
164
|
+
description: "Un ami à Seyda Neen"
|
|
165
|
+
activator:
|
|
166
|
+
stump: "Souche Creuse"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Build with the default locale:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
bin/esp build MyMod # -> dist/MyMod.esp (English)
|
|
173
|
+
bin/esp build MyMod --locale fr # -> dist/MyMod.fr.esp (French)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Missing keys fall back to `en` with a warning in the build logs.
|
|
177
|
+
`esp i18n check MyMod` reports missing and orphan keys per locale.
|
|
178
|
+
|
|
179
|
+
## Linting
|
|
180
|
+
|
|
181
|
+
`esp lint <Mod>` checks Npc and scripted-object records for dangling
|
|
182
|
+
references — script / race / class / faction IDs that don't exist in
|
|
183
|
+
the mod itself or in any indexed vanilla ESM. Two severities:
|
|
184
|
+
|
|
185
|
+
- `ERROR` — ref points at nothing the mod or vanilla defines.
|
|
186
|
+
- `WARN` — ref resolves in a vanilla ESM that isn't listed in the
|
|
187
|
+
mod's `Header.masters`. OpenMW will still load it but log warnings.
|
|
188
|
+
|
|
189
|
+
Non-zero exit on any error — wire it into CI.
|
|
190
|
+
|
|
191
|
+
## Authoring with AI
|
|
192
|
+
|
|
193
|
+
Since every command accepts `--json` and `esp docs introspect` dumps the
|
|
194
|
+
full capability surface, an AI agent can:
|
|
195
|
+
|
|
196
|
+
1. Call `esp docs introspect` to learn the available commands.
|
|
197
|
+
2. Read records via `esp refs find <id> --show --json`.
|
|
198
|
+
3. Author a mod source file (Ruby/Python/JSON/whatever).
|
|
199
|
+
4. Validate via `esp lint <Mod> --json`.
|
|
200
|
+
5. Build via `esp build <Mod> --json`.
|
|
201
|
+
|
|
202
|
+
`esp mcp serve` exposes this loop as first-class MCP tools; see
|
|
203
|
+
[getting-started](getting-started.md#wiring-esp-into-an-ai-client-mcp)
|
|
204
|
+
for wiring it into an AI client, and the
|
|
205
|
+
[walkthrough](walkthrough.md#same-pipeline-three-frontends) for the loop
|
|
206
|
+
in action.
|