Module: Esp::Mw::Operations
- Defined in:
- lib/esp/mw/operations.rb
Overview
The Morrowind plugin’s half of the service layer. Same contract as Esp::Operations (string-keyed params hash in, plain Hash out) — these are the ops that decode TES3 records, drive tes3conv, or otherwise know what a “mod” is. Frontends never call this module directly; they route through Esp::Operations.dispatch(op, input).
Every op resolves the project root from ‘Esp::ActiveProject.resolve` (step 23.5 slice 1) so an `open_project` request actually moves where build/lint/etc. operate — they no longer silently target Esp::ROOT. Precedence: explicit `root:` in params > active project > Esp::ROOT.
InputError is aliased from the shell so plugin ops raise the same class the frontends rescue. Loader / Preflight / Tes3conv error classes are registered with Esp::Operations::ERROR_CODES at the bottom of this file — they couldn’t be in the shell table because the plugin hadn’t loaded yet when that table was built.
Constant Summary collapse
- InputError =
Esp::Operations::InputError
- PLUGIN_EXT =
/\.(esp|esm|omwaddon)\z/i
Class Method Summary collapse
- .build(params) ⇒ Object
-
.build_all(params) ⇒ Object
Build every mod under mods/.
-
.dialogue_write(params) ⇒ Object
Author dialogue from a JSON spec (the data-driven analog of the Ruby ‘dialogue { }` DSL) into a mod’s .json source.
- .extract_scripts(params) ⇒ Object
- .i18n_check(params) ⇒ Object
-
.install(params) ⇒ Object
Make a built dist/<mod>.esp available to a game engine.
- .lint(params) ⇒ Object
-
.plugins_list(params = {}) ⇒ Object
List plugins installed in the OpenMW config’s data directories, with active flag + load order.
-
.record_write(params) ⇒ Object
Upsert one record into a mod’s .json source, keyed by (type, id).
-
.records_read(params) ⇒ Object
Read a mod’s records as structured data.
- .refs_find(params) ⇒ Object
-
.refs_resolve(params) ⇒ Object
Map a batch of reference ids → record type via the SQLite reference index.
- .scaffold(params) ⇒ Object
-
.unpack(params) ⇒ Object
Import an existing plugin into source.
Class Method Details
.build(params) ⇒ Object
28 29 30 31 |
# File 'lib/esp/mw/operations.rb', line 28 def build(params) mod = require_mod(params) build_one(mod, params['locale'], root: project_root(params)) end |
.build_all(params) ⇒ Object
Build every mod under mods/. Returns one entry per mod, same shape as a single build, so an agent can fan out without N calls.
35 36 37 38 39 40 41 |
# File 'lib/esp/mw/operations.rb', line 35 def build_all(params) root = project_root(params) results = Esp::Mw::Builder.discover_mods(root: root).map do |mod| build_one(mod, params['locale'], root: root) end { results: results } end |
.dialogue_write(params) ⇒ Object
Author dialogue from a JSON spec (the data-driven analog of the Ruby ‘dialogue { }` DSL) into a mod’s .json source. A topic is written wholesale: any existing DIAL/INFO records for the same topics are replaced, so re-authoring never leaves orphan info records.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/esp/mw/operations.rb', line 155 def dialogue_write(params) mod = require_mod(params) spec = params['spec'] unless spec.is_a?(Hash) || spec.is_a?(Array) raise InputError, Esp.t('errors.operations.missing_object', field: 'spec') end built = Esp::Mw::DialogueDsl.from_spec(spec) topics = built.select { |r| r['type'] == 'Dialogue' }.map { |r| r['id'] } root = project_root(params) source = json_source!(mod, root: root) records = strip_dialogue(JSON.parse(File.read(source)), topics).concat(built) File.write(source, "#{JSON.pretty_generate(records)}\n") { mod: mod, source: Esp::Operations.relative(source, root: root), topics: topics, records_written: built.size, records: built } end |
.extract_scripts(params) ⇒ Object
201 202 203 204 205 |
# File 'lib/esp/mw/operations.rb', line 201 def extract_scripts(params) mod = require_mod(params) result = Esp::Mw::ScriptExtractor.extract!(mod, root: project_root(params)) { mod: mod, extracted: result.extracted, skipped: result.skipped } end |
.i18n_check(params) ⇒ Object
106 107 108 109 110 111 |
# File 'lib/esp/mw/operations.rb', line 106 def i18n_check(params) mod = require_mod(params) source = Esp::Mw::Loader.resolve(mod, root: project_root(params)) { mod: mod, default_locale: Esp::Mw::I18n::DEFAULT_LOCALE, locales: Esp::Mw::I18n.check(File.dirname(source)) } end |
.install(params) ⇒ Object
Make a built dist/<mod>.esp available to a game engine. Two additive install paths (compose freely):
-
OpenMW (default): register the project’s dist/ + the plugin with openmw.cfg via ‘data=` + `content=`. Idempotent —an already-present line reports `added: false`. Skipped when `params` is explicitly false (e.g. an original-engine user who only wants the copy).
-
**Copy to a Data Files dir** (original engine, step 23.5 slice 4): pass ‘params` with a path, or `params => true` to auto-resolve the user’s vanilla Morrowind Data Files dir via Esp::Mw::DataFiles. The copy is “always overwrite” so a rebuild swaps the live plugin without the user touching anything.
‘data=` always points at the active project’s dist/, so OpenMW picks up the build output from wherever the project lives.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/esp/mw/operations.rb', line 80 def install(params) mod = require_mod(params) root = project_root(params) esp = File.join(root, 'dist', "#{mod}.esp") unless File.exist?(esp) raise InputError, Esp.t('errors.operations.build_first', esp: Esp::Operations.relative(esp, root: root)) end payload = { mod: mod } payload.merge!(register_openmw(mod, root, params)) unless params['register_openmw'] == false if (target = copy_target(params)) payload[:copied_to] = copy_built_plugin(esp, mod, target) end payload end |
.lint(params) ⇒ Object
172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/esp/mw/operations.rb', line 172 def lint(params) mod = require_mod(params) index = require_index! records = Esp::Mw::Loader.load(Esp::Mw::Loader.resolve(mod, root: project_root(params))) issues = Esp::Mw::Linter.new(records, index).issues { mod: mod, errors: issues.count { |i| i.severity == :error }, warnings: issues.count { |i| i.severity == :warning }, issues: issues.map(&:to_h) } end |
.plugins_list(params = {}) ⇒ Object
List plugins installed in the OpenMW config’s data directories, with active flag + load order. ‘config` overrides the cfg path.
101 102 103 104 |
# File 'lib/esp/mw/operations.rb', line 101 def plugins_list(params = {}) cfg = openmw_config(params) { openmw_cfg: cfg.path, exists: cfg.exist?, plugins: cfg.installed_plugins } end |
.record_write(params) ⇒ Object
Upsert one record into a mod’s .json source, keyed by (type, id). JSON only: polyglot sources are programs, not editable data, so we refuse rather than silently no-op. Header is matched by type alone (one per file); every other record needs an id.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/esp/mw/operations.rb', line 128 def record_write(params) mod = require_mod(params) record = params['record'] unless record.is_a?(Hash) raise InputError, Esp.t('errors.operations.missing_object', field: 'record') end type = record['type'] raise InputError, Esp.t('errors.operations.record_needs_type') if blank?(type) if type != 'Header' && blank?(record['id']) raise InputError, Esp.t('errors.operations.record_needs_id') end root = project_root(params) source = json_source!(mod, root: root) records = JSON.parse(File.read(source)) action, index = upsert(records, record) File.write(source, "#{JSON.pretty_generate(records)}\n") { mod: mod, source: Esp::Operations.relative(source, root: root), action: action, type: type, id: record['id'], index: index } end |
.records_read(params) ⇒ Object
Read a mod’s records as structured data. Format-agnostic — runs the source through the loader, so .rb/.py/.js/.ts all read back too.
115 116 117 118 119 120 121 122 |
# File 'lib/esp/mw/operations.rb', line 115 def records_read(params) mod = require_mod(params) root = project_root(params) source = Esp::Mw::Loader.resolve(mod, root: root) records = Esp::Mw::Loader.load(source) { mod: mod, source: Esp::Operations.relative(source, root: root), count: records.size, records: records } end |
.refs_find(params) ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/esp/mw/operations.rb', line 207 def refs_find(params) index = require_index! criteria = { query: params['q'], type: params['type'], like: params['like'], exact: params['exact'] } rows = index.find(**criteria, limit: (params['limit'] || 100).to_i) total = index.count_matching(**criteria) if params['show'] records = rows.map { |r| index.fetch_record(r['source_esm'], r['record_index']) } { records: records, count: records.size, total: total } else { matches: rows, count: rows.size, total: total } end end |
.refs_resolve(params) ⇒ Object
Map a batch of reference ids → record type via the SQLite reference index. Backs the cell-view marker colouring (step 21 slice 3) — one call resolves all of a cell’s references in a single SQL.
224 225 226 227 228 |
# File 'lib/esp/mw/operations.rb', line 224 def refs_resolve(params) index = require_index! ids = Array(params['ids']).map(&:to_s) { types: index.types_for(ids) } end |
.scaffold(params) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/esp/mw/operations.rb', line 185 def scaffold(params) mod = require_mod(params) root = project_root(params) result = Esp::Mw::Scaffolder.create( mod, format: params['format'] || Esp::Mw::Scaffolder::DEFAULT_FORMAT, author: params['author'], description: params['description'], force: params['force'] || false, root: root ) { mod: result.mod, format: result.format, source: Esp::Operations.relative(result.source, root: root), readme: Esp::Operations.relative(result.readme, root: root) } end |
.unpack(params) ⇒ Object
Import an existing plugin into source. ‘plugin` is either a filesystem path or, if it’s a bare name (no path separator), a plugin name resolved against the OpenMW config’s installed plugins. ‘name` defaults to the resolved file’s basename. Source mod lands under the active project’s mods/, not the toolchain repo’s.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/esp/mw/operations.rb', line 48 def unpack(params) plugin = params['plugin'] raise InputError, Esp.t('errors.operations.missing_field', field: 'plugin') if blank?(plugin) source = resolve_plugin(plugin, params) raise InputError, Esp.t('errors.operations.plugin_not_found', plugin: plugin) unless source root = project_root(params) name = params['name'] || File.basename(source).sub(PLUGIN_EXT, '') dst = File.join(root, 'mods', name, "#{name}.json") FileUtils.mkdir_p(File.dirname(dst)) Esp::Mw::Tes3conv.convert(source, dst) { plugin: plugin, source: source, mod: name, output: Esp::Operations.relative(dst, root: root) } end |