@nilvn/core 0.14.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 +21 -0
- package/README.md +74 -0
- package/dist/catalog.d.ts +54 -0
- package/dist/catalog.js +137 -0
- package/dist/chunk-build.d.ts +37 -0
- package/dist/chunk-build.js +187 -0
- package/dist/chunk.d.ts +106 -0
- package/dist/chunk.js +35 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +181 -0
- package/dist/i18n.d.ts +50 -0
- package/dist/i18n.js +77 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/ir.d.ts +407 -0
- package/dist/ir.js +167 -0
- package/dist/package.d.ts +100 -0
- package/dist/package.js +80 -0
- package/dist/plugin-manifest.d.ts +146 -0
- package/dist/plugin-manifest.js +165 -0
- package/dist/plugins.d.ts +26 -0
- package/dist/plugins.js +42 -0
- package/dist/schema.d.ts +73 -0
- package/dist/schema.js +15 -0
- package/dist/screenplay-format.d.ts +8 -0
- package/dist/screenplay-format.js +96 -0
- package/dist/screenplay.d.ts +99 -0
- package/dist/screenplay.js +161 -0
- package/dist/semver.d.ts +15 -0
- package/dist/semver.js +129 -0
- package/dist/serialize.d.ts +63 -0
- package/dist/serialize.js +365 -0
- package/dist/versions.d.ts +17 -0
- package/dist/versions.js +25 -0
- package/package.json +42 -0
package/dist/package.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Script package (.nvs) — the contract core of "a work the engine can load and
|
|
2
|
+
// run on its own". One `nilvn.json`
|
|
3
|
+
// carries everything a shell used to split across index.html / boot.json /
|
|
4
|
+
// manifest.json: work metadata, the actor table, the enabled plugin set, the
|
|
5
|
+
// initial + switchable languages, playback defaults, and the chunk manifest
|
|
6
|
+
// (embedded verbatim, so every ContentLoader keeps reading a ChunkManifest).
|
|
7
|
+
//
|
|
8
|
+
// Pure types + a builder over buildChunkedExport, zero-dep. The editor's four
|
|
9
|
+
// exports (single-file HTML / asset ZIP / chunked ZIP / .nvpk) are this package
|
|
10
|
+
// plus a shell; the engine's `load()` is its one consumer.
|
|
11
|
+
import { isChunkManifest } from './chunk.js';
|
|
12
|
+
import { buildChunkedExport } from './chunk-build.js';
|
|
13
|
+
/** Script-package format version. Bumped only when nilvn.json's shape changes
|
|
14
|
+
* incompatibly; the embedded chunk manifest keeps its own `format`. */
|
|
15
|
+
export const PACKAGE_FORMAT = 1;
|
|
16
|
+
/** The package's single entry file, at the package root. */
|
|
17
|
+
export const PACKAGE_MANIFEST_FILE = 'nilvn.json';
|
|
18
|
+
/** Structural check for a parsed nilvn.json — a foreign or truncated file fails
|
|
19
|
+
* cleanly here instead of deep inside playback. Does NOT check `format`
|
|
20
|
+
* numbers (callers gate those so they can word the mismatch message). */
|
|
21
|
+
export function isPackageManifest(x) {
|
|
22
|
+
if (!x || typeof x !== 'object')
|
|
23
|
+
return false;
|
|
24
|
+
const m = x;
|
|
25
|
+
return (typeof m.format === 'number' &&
|
|
26
|
+
typeof m.title === 'string' &&
|
|
27
|
+
typeof m.engine === 'string' &&
|
|
28
|
+
typeof m.lang === 'string' &&
|
|
29
|
+
Array.isArray(m.languages) &&
|
|
30
|
+
!!m.actors &&
|
|
31
|
+
typeof m.actors === 'object' &&
|
|
32
|
+
Array.isArray(m.plugins) &&
|
|
33
|
+
typeof m.textSpeed === 'number' &&
|
|
34
|
+
typeof m.saveKey === 'string' &&
|
|
35
|
+
isChunkManifest(m.chunks));
|
|
36
|
+
}
|
|
37
|
+
/** Languages the finished work can switch between: the default plus every
|
|
38
|
+
* declared language that ships a non-empty catalog. The default comes first. */
|
|
39
|
+
export function packageLanguages(project) {
|
|
40
|
+
const def = project.meta.defaultLang;
|
|
41
|
+
const declared = project.meta.languages?.length ? project.meta.languages : [def];
|
|
42
|
+
const extra = declared.filter((l) => l !== def && Object.keys(project.catalogs[l] ?? {}).length > 0);
|
|
43
|
+
return [def, ...extra];
|
|
44
|
+
}
|
|
45
|
+
/** The runtime actor table straight from the IR (`name` = the default-language
|
|
46
|
+
* display name, falling back to the id). */
|
|
47
|
+
export function packageActors(project) {
|
|
48
|
+
const cat = project.catalogs[project.meta.defaultLang] ?? {};
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [id, a] of Object.entries(project.actors)) {
|
|
51
|
+
out[id] = { name: cat[a.nameKey] ?? id, nameKey: a.nameKey, color: a.color, sprites: a.sprites, defaultFace: a.defaultFace, voice: a.voice };
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Build a script package from a project: the chunked script side (one chunk per
|
|
56
|
+
* scene by default; `groups` merges scenes — a single group = one chunk, the
|
|
57
|
+
* single-file / asset-ZIP shape) plus the manifest fields the shells used to bake
|
|
58
|
+
* into their bootstraps. Pure, zero-I/O. */
|
|
59
|
+
export function buildScriptPackage(project, opts) {
|
|
60
|
+
const plan = buildChunkedExport(project, { engine: opts.engine, groups: opts.groups });
|
|
61
|
+
const title = opts.title ?? (project.meta.title || 'NilVN');
|
|
62
|
+
const lang = (opts.lang ?? project.meta.defaultLang);
|
|
63
|
+
const manifest = {
|
|
64
|
+
format: PACKAGE_FORMAT,
|
|
65
|
+
title,
|
|
66
|
+
engine: opts.engine,
|
|
67
|
+
lang,
|
|
68
|
+
languages: opts.languages ?? packageLanguages(project),
|
|
69
|
+
actors: opts.actors ?? packageActors(project),
|
|
70
|
+
plugins: opts.plugins ?? project.plugins.map((p) => ({ id: p.id })),
|
|
71
|
+
textSpeed: opts.textSpeed ?? (Number(project.meta.textSpeed) || 40),
|
|
72
|
+
saveKey: opts.saveKey ?? (project.meta.id || project.meta.title || 'nilvn'),
|
|
73
|
+
chunks: plan.manifest,
|
|
74
|
+
};
|
|
75
|
+
return { manifest, files: plan.files, assetRefs: plan.assetRefs };
|
|
76
|
+
}
|
|
77
|
+
/** Fill the by-ref asset table (`chunks.assets`). Returns a new manifest. */
|
|
78
|
+
export function fillPackageAssets(manifest, assets) {
|
|
79
|
+
return { ...manifest, chunks: { ...manifest.chunks, assets } };
|
|
80
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { CommandSchema, EffectSchema, ObjectKindSchema, TextEffectDef } from './schema.js';
|
|
2
|
+
/** The plugin↔host contract version. A manifest declaring a higher `apiVersion`
|
|
3
|
+
* than the host supports is rejected (forward-compat is not guaranteed). v1 was
|
|
4
|
+
* the pre-batch-B `{ name, entry, capabilities }` shape; v2 = this file. */
|
|
5
|
+
export declare const PLUGIN_API_VERSION = 2;
|
|
6
|
+
/** The file a plugin package is rooted at. */
|
|
7
|
+
export declare const PLUGIN_MANIFEST_FILE = "plugin.json";
|
|
8
|
+
/** The `entries.*` sentinel host-registry plugins use: the host's own registry
|
|
9
|
+
* supplies the module (the first-party set, or a module the host registered
|
|
10
|
+
* itself), nothing is fetched. */
|
|
11
|
+
export declare const BUNDLED_ENTRY = "bundled";
|
|
12
|
+
export type PermissionSide = 'engine' | 'editor' | 'host';
|
|
13
|
+
export type CatalogStatus = 'active' | 'reserved';
|
|
14
|
+
export interface PermissionDef {
|
|
15
|
+
/** The id a manifest writes; `pattern` ids end in `:` and take a suffix. */
|
|
16
|
+
id: string;
|
|
17
|
+
group: string;
|
|
18
|
+
side: PermissionSide;
|
|
19
|
+
status: CatalogStatus;
|
|
20
|
+
pattern?: boolean;
|
|
21
|
+
/** What the capability object exposes (active) or is reserved for. */
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
/** The closed capability catalog. */
|
|
25
|
+
export declare const PERMISSIONS: readonly PermissionDef[];
|
|
26
|
+
/** A permission id as a manifest writes it (pattern ids carry their suffix). */
|
|
27
|
+
export type Permission = 'stage.read' | 'stage.write' | `stage.layer:${string}` | 'audio.play' | `audio.bus:${string}` | 'audio.capture' | 'vars.read' | 'vars.write' | 'save.slice' | 'session.save' | 'session.settings' | 'session.backlog' | 'session.replay' | 'ui.layer' | 'ui.panel' | 'ui.window' | 'ui.toast' | 'ui.inspector' | 'project.read' | 'project.commit' | 'assets.read' | 'assets.write' | 'fs.pick' | 'fs.project' | `net:${string}` | 'clipboard' | 'timer' | 'ai.text' | 'ai.image' | 'ai.audio' | 'ai.code' | 'ai.context';
|
|
28
|
+
/** Resolve a permission id (exact, or a pattern id with its suffix) to its
|
|
29
|
+
* catalog entry; undefined for anything outside the catalog. */
|
|
30
|
+
export declare function matchPermission(id: string): PermissionDef | undefined;
|
|
31
|
+
export interface ExtensionPointDef {
|
|
32
|
+
key: string;
|
|
33
|
+
side: 'engine' | 'editor';
|
|
34
|
+
status: CatalogStatus;
|
|
35
|
+
/** Schema version of this point's contribution shape. */
|
|
36
|
+
version: number;
|
|
37
|
+
description: string;
|
|
38
|
+
}
|
|
39
|
+
/** The keys `contributes` may carry. */
|
|
40
|
+
export declare const EXTENSION_POINTS: readonly ExtensionPointDef[];
|
|
41
|
+
/** A manager panel the editor hosts for the plugin (was `PluginUI`). */
|
|
42
|
+
export interface PanelDef {
|
|
43
|
+
id: string;
|
|
44
|
+
/** Tab label — an i18n id resolved through the plugin's `messages`. */
|
|
45
|
+
label: string;
|
|
46
|
+
/** Free-form descriptor; the plugin renders its own panel body. */
|
|
47
|
+
kind?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Contributions per extension point. Every field optional; unknown keys are
|
|
50
|
+
* tolerated (ignored with a warning) so a newer plugin loads on an older host. */
|
|
51
|
+
export interface PluginContributions {
|
|
52
|
+
commands?: CommandSchema[];
|
|
53
|
+
textEffects?: TextEffectDef[];
|
|
54
|
+
/** Object kinds this plugin contributes (declarative mirror of the engine's
|
|
55
|
+
* `objectKinds`). Built-in kinds live in BUILTIN_OBJECT_KINDS, not here. */
|
|
56
|
+
objectKinds?: ObjectKindSchema[];
|
|
57
|
+
effects?: EffectSchema[];
|
|
58
|
+
hooks?: string[];
|
|
59
|
+
saveSlice?: boolean;
|
|
60
|
+
panels?: PanelDef[];
|
|
61
|
+
nodeKinds?: string[];
|
|
62
|
+
stageTools?: string[];
|
|
63
|
+
lineActions?: string[];
|
|
64
|
+
objectMenu?: string[];
|
|
65
|
+
/** Reserved points (see EXTENSION_POINTS) — typed loosely on purpose. */
|
|
66
|
+
[reserved: string]: unknown;
|
|
67
|
+
}
|
|
68
|
+
export interface PluginActivation {
|
|
69
|
+
/** `eager` (default): activate when enabled; `onCommand`: at the first
|
|
70
|
+
* execution of one of its `contributes.commands`; `manual`: only via
|
|
71
|
+
* `engine.enablePlugin`. */
|
|
72
|
+
engine?: 'eager' | 'onCommand' | 'manual';
|
|
73
|
+
editor?: 'eager' | 'onPanelOpen';
|
|
74
|
+
}
|
|
75
|
+
/** `plugin.json` — what a plugin tells the hosts so users can enable and use it
|
|
76
|
+
* by clicking, and so the hosts can run it with exactly the capabilities it
|
|
77
|
+
* declared. Superset of the v1 manifest; `id` is the stable key everywhere
|
|
78
|
+
* (`name` is display only). */
|
|
79
|
+
export interface PluginManifest {
|
|
80
|
+
/** Stable reverse-DNS id (`app.nilvn.textfx`). The enabled-set / `[use]` /
|
|
81
|
+
* i18n-namespace key. */
|
|
82
|
+
id: string;
|
|
83
|
+
/** Display name: an i18n id resolved through `messages` (`plugin.textfx.name`). */
|
|
84
|
+
name: string;
|
|
85
|
+
/** An i18n id (`plugin.textfx.desc`). */
|
|
86
|
+
description?: string;
|
|
87
|
+
/** The plugin's own SemVer. */
|
|
88
|
+
version: string;
|
|
89
|
+
/** Engine compatibility range (semver.ts subset); absent = any. */
|
|
90
|
+
engine?: string;
|
|
91
|
+
/** Editor compatibility range; absent = any. */
|
|
92
|
+
editor?: string;
|
|
93
|
+
/** Other plugins this one needs: id → range. Activated first; a missing or
|
|
94
|
+
* incompatible dependency keeps this plugin inactive with a diagnostic. */
|
|
95
|
+
dependencies?: Record<string, string>;
|
|
96
|
+
contributes?: PluginContributions;
|
|
97
|
+
/** Capabilities requested from the PERMISSIONS catalog. */
|
|
98
|
+
permissions?: Permission[];
|
|
99
|
+
activation?: PluginActivation;
|
|
100
|
+
/** Hot-plug policy: `hot` (default) = deactivate / dispose / reactivate in
|
|
101
|
+
* place; `restart` = the host must reload (IR node shapes, renderer layers…). */
|
|
102
|
+
reload?: 'hot' | 'restart';
|
|
103
|
+
/** Module entries relative to plugin.json (`'bundled'` for host-registry
|
|
104
|
+
* plugins). No `engine` = a pure editor plugin (the former `editorOnly`); no
|
|
105
|
+
* `editor` = a pure runtime plugin. */
|
|
106
|
+
entries?: {
|
|
107
|
+
engine?: string;
|
|
108
|
+
editor?: string;
|
|
109
|
+
};
|
|
110
|
+
/** Stylesheets relative to plugin.json, scoped by the host per plugin id. */
|
|
111
|
+
styles?: string[];
|
|
112
|
+
/** The plugin's own UI-chrome i18n catalog: `{ lang: { id: text } }`. The hosts
|
|
113
|
+
* register it under the `plugin:<id>` namespace. */
|
|
114
|
+
messages?: Partial<Record<string, Record<string, string>>>;
|
|
115
|
+
/** Author-facing usage block (one display line per entry): how to trigger
|
|
116
|
+
* this plugin in an AuthoringDoc. Tools prefer it over deriving usage from
|
|
117
|
+
* `contributes`, which under-reports hook-only / auto plugins. */
|
|
118
|
+
authorUsage?: string[];
|
|
119
|
+
/** Plugin contract version this manifest targets (defaults to current). */
|
|
120
|
+
apiVersion?: number;
|
|
121
|
+
}
|
|
122
|
+
/** `id` grammar: reverse-DNS, ≥ 2 dot-separated labels of `[a-z0-9-]`, each
|
|
123
|
+
* starting with a letter. */
|
|
124
|
+
export declare const PLUGIN_ID_RE: RegExp;
|
|
125
|
+
export declare function isPluginId(s: string): boolean;
|
|
126
|
+
export interface ManifestReport {
|
|
127
|
+
/** Problems that reject the manifest. */
|
|
128
|
+
errors: string[];
|
|
129
|
+
/** Tolerated oddities (unknown extension points, reserved permissions…). */
|
|
130
|
+
warnings: string[];
|
|
131
|
+
}
|
|
132
|
+
export interface ValidateManifestOptions {
|
|
133
|
+
/** Host versions to check the manifest's ranges against (absent = skip). */
|
|
134
|
+
engineVersion?: string;
|
|
135
|
+
editorVersion?: string;
|
|
136
|
+
}
|
|
137
|
+
/** Load-time check of a manifest against the host's catalogs, contract version
|
|
138
|
+
* and (optionally) versions. Bundled manifests always pass with no warnings;
|
|
139
|
+
* this is the seam that guards third-party plugins. */
|
|
140
|
+
export declare function validatePluginManifest(m: PluginManifest, opts?: ValidateManifestOptions): ManifestReport;
|
|
141
|
+
/** Convenience: only the errors (the shape most call sites need). */
|
|
142
|
+
export declare function manifestErrors(m: PluginManifest, opts?: ValidateManifestOptions): string[];
|
|
143
|
+
/** Whether the manifest ships a runtime (engine) half. */
|
|
144
|
+
export declare function hasEngineHalf(m: PluginManifest): boolean;
|
|
145
|
+
/** Whether the manifest ships an editor half. */
|
|
146
|
+
export declare function hasEditorHalf(m: PluginManifest): boolean;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Plugin manifest v2 (`plugin.json`) — the declarative half of the plugin
|
|
2
|
+
// platform.
|
|
3
|
+
// A manifest says WHAT a plugin contributes (extension points), WHAT it needs
|
|
4
|
+
// (permissions) and HOW the host should run it (activation / reload / entries);
|
|
5
|
+
// the executable halves live in the engine (`entries.engine`) and the editor
|
|
6
|
+
// (`entries.editor`). Host-registry plugins (the first-party set in
|
|
7
|
+
// @nilvn/plugins, or any module a host registers itself) use the `'bundled'`
|
|
8
|
+
// entry sentinel — the hosts resolve those from their own registries.
|
|
9
|
+
//
|
|
10
|
+
// Two catalogs are the host-owned vocabulary a plugin may NOT extend:
|
|
11
|
+
// EXTENSION_POINTS — the keys `contributes` may carry (unknown keys are
|
|
12
|
+
// ignored with a warning: forward-compatible);
|
|
13
|
+
// PERMISSIONS — the capability ids `permissions` may request; a plugin
|
|
14
|
+
// only ever receives the capability objects it declared AND
|
|
15
|
+
// the host granted (the rest are simply `undefined`).
|
|
16
|
+
import { isValidRange, satisfiesRange } from './semver.js';
|
|
17
|
+
/** The plugin↔host contract version. A manifest declaring a higher `apiVersion`
|
|
18
|
+
* than the host supports is rejected (forward-compat is not guaranteed). v1 was
|
|
19
|
+
* the pre-batch-B `{ name, entry, capabilities }` shape; v2 = this file. */
|
|
20
|
+
export const PLUGIN_API_VERSION = 2;
|
|
21
|
+
/** The file a plugin package is rooted at. */
|
|
22
|
+
export const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
23
|
+
/** The `entries.*` sentinel host-registry plugins use: the host's own registry
|
|
24
|
+
* supplies the module (the first-party set, or a module the host registered
|
|
25
|
+
* itself), nothing is fetched. */
|
|
26
|
+
export const BUNDLED_ENTRY = 'bundled';
|
|
27
|
+
/** The closed capability catalog. */
|
|
28
|
+
export const PERMISSIONS = [
|
|
29
|
+
{ id: 'stage.read', group: 'stage', side: 'engine', status: 'active', description: 'Read stage objects: hasObject / getProp / getBand / getFace / hasChar / charFace / snapshot.' },
|
|
30
|
+
{ id: 'stage.write', group: 'stage', side: 'engine', status: 'active', description: 'Stage read + every Renderer write verb, applyEffect, and keyframe choreography (playFrames / startLoop / stopLoop).' },
|
|
31
|
+
{ id: 'stage.layer:', group: 'stage', side: 'engine', status: 'reserved', pattern: true, description: 'A named renderer layer (weather / masks).' },
|
|
32
|
+
{ id: 'audio.play', group: 'audio', side: 'engine', status: 'active', description: 'playTrack / stopTrack / stopAllTracks / playSe, channel volumes and voicePlaying.' },
|
|
33
|
+
{ id: 'audio.bus:', group: 'audio', side: 'engine', status: 'reserved', pattern: true, description: 'A specific audio bus.' },
|
|
34
|
+
{ id: 'audio.capture', group: 'audio', side: 'host', status: 'reserved', description: 'Microphone capture (host-mediated).' },
|
|
35
|
+
{ id: 'vars.read', group: 'data', side: 'engine', status: 'active', description: 'Read script variables.' },
|
|
36
|
+
{ id: 'vars.write', group: 'data', side: 'engine', status: 'active', description: 'Read + write script variables.' },
|
|
37
|
+
{ id: 'save.slice', group: 'data', side: 'engine', status: 'active', description: 'Own a SaveState.ext slice (saveState / restoreState are called).' },
|
|
38
|
+
{ id: 'session.save', group: 'session', side: 'engine', status: 'active', description: 'saveState / restoreState / restart, the work save key and build info.' },
|
|
39
|
+
{ id: 'session.settings', group: 'session', side: 'engine', status: 'active', description: 'Text speed, channel volumes, language switching, resolveText.' },
|
|
40
|
+
{ id: 'session.backlog', group: 'session', side: 'engine', status: 'active', description: 'The dialogue backlog and voice replay by ref.' },
|
|
41
|
+
{ id: 'session.replay', group: 'session', side: 'engine', status: 'active', description: 'A–B replay segments: list / play / end / seen signals.' },
|
|
42
|
+
{ id: 'ui.layer', group: 'ui', side: 'engine', status: 'active', description: 'A host container inside the stage root (removed on dispose).' },
|
|
43
|
+
{ id: 'ui.panel', group: 'ui', side: 'editor', status: 'active', description: 'A manager panel host in the editor.' },
|
|
44
|
+
{ id: 'ui.window', group: 'ui', side: 'editor', status: 'active', description: 'A draggable editor window.' },
|
|
45
|
+
{ id: 'ui.toast', group: 'ui', side: 'editor', status: 'active', description: 'Editor toasts.' },
|
|
46
|
+
{ id: 'ui.inspector', group: 'ui', side: 'editor', status: 'reserved', description: 'The selected-object inspector panel.' },
|
|
47
|
+
{ id: 'project.read', group: 'project', side: 'editor', status: 'active', description: 'Read the open project (IR) and editor state.' },
|
|
48
|
+
{ id: 'project.commit', group: 'project', side: 'editor', status: 'active', description: 'Mutate the project through the editor (undoable).' },
|
|
49
|
+
{ id: 'assets.read', group: 'project', side: 'editor', status: 'active', description: 'Resolve project assets.' },
|
|
50
|
+
{ id: 'assets.write', group: 'project', side: 'editor', status: 'active', description: 'Import / replace project assets.' },
|
|
51
|
+
{ id: 'fs.pick', group: 'system', side: 'host', status: 'reserved', description: 'Native file picker (host-mediated).' },
|
|
52
|
+
{ id: 'fs.project', group: 'system', side: 'host', status: 'reserved', description: 'Files under the project directory (host-mediated).' },
|
|
53
|
+
{ id: 'net:', group: 'system', side: 'host', status: 'reserved', pattern: true, description: 'Network access to one origin (host-mediated; third-party default-deny).' },
|
|
54
|
+
{ id: 'clipboard', group: 'system', side: 'host', status: 'reserved', description: 'Clipboard read / write.' },
|
|
55
|
+
{ id: 'timer', group: 'system', side: 'engine', status: 'active', description: 'setTimeout / setInterval / requestAnimationFrame, cleared on dispose.' },
|
|
56
|
+
{ id: 'ai.text', group: 'ai', side: 'host', status: 'reserved', description: 'Text generation through the configured provider.' },
|
|
57
|
+
{ id: 'ai.image', group: 'ai', side: 'host', status: 'reserved', description: 'Image generation.' },
|
|
58
|
+
{ id: 'ai.audio', group: 'ai', side: 'host', status: 'reserved', description: 'Audio / voice generation.' },
|
|
59
|
+
{ id: 'ai.code', group: 'ai', side: 'host', status: 'reserved', description: 'Code generation (plugins from natural language).' },
|
|
60
|
+
{ id: 'ai.context', group: 'ai', side: 'host', status: 'reserved', description: 'Read the authoring context handed to AI slots.' },
|
|
61
|
+
];
|
|
62
|
+
/** Resolve a permission id (exact, or a pattern id with its suffix) to its
|
|
63
|
+
* catalog entry; undefined for anything outside the catalog. */
|
|
64
|
+
export function matchPermission(id) {
|
|
65
|
+
for (const p of PERMISSIONS) {
|
|
66
|
+
if (p.pattern ? id.startsWith(p.id) && id.length > p.id.length : id === p.id)
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** The keys `contributes` may carry. */
|
|
72
|
+
export const EXTENSION_POINTS = [
|
|
73
|
+
{ key: 'commands', side: 'engine', status: 'active', version: 1, description: 'Script commands `[name …]` (CommandSchema drives the insert form).' },
|
|
74
|
+
{ key: 'textEffects', side: 'engine', status: 'active', version: 1, description: 'Inline text effects `{name:text}`.' },
|
|
75
|
+
{ key: 'objectKinds', side: 'engine', status: 'active', version: 1, description: 'Addressable stage-object kinds.' },
|
|
76
|
+
{ key: 'effects', side: 'engine', status: 'active', version: 1, description: 'Retargetable effects bound to kinds via appliesToKinds.' },
|
|
77
|
+
{ key: 'hooks', side: 'engine', status: 'active', version: 1, description: 'Engine hook names the runtime half listens to (introspection; the module is authoritative).' },
|
|
78
|
+
{ key: 'saveSlice', side: 'engine', status: 'active', version: 1, description: 'Declares a SaveState.ext slice owned by this plugin.' },
|
|
79
|
+
{ key: 'rendererLayers', side: 'engine', status: 'reserved', version: 1, description: 'Named renderer layers.' },
|
|
80
|
+
{ key: 'panels', side: 'editor', status: 'active', version: 1, description: 'Manager panels (the editor renders the host; the plugin fills it).' },
|
|
81
|
+
{ key: 'nodeKinds', side: 'editor', status: 'active', version: 1, description: 'IR node kinds this plugin owns (forms + inert flag while disabled).' },
|
|
82
|
+
{ key: 'stageTools', side: 'editor', status: 'active', version: 1, description: 'Stage overlay tools (modal takeovers of the stage).' },
|
|
83
|
+
{ key: 'lineActions', side: 'editor', status: 'active', version: 1, description: 'Buttons on a dialogue line’s action bar.' },
|
|
84
|
+
{ key: 'objectMenu', side: 'editor', status: 'active', version: 1, description: 'Sections in an on-stage object’s context menu.' },
|
|
85
|
+
{ key: 'windows', side: 'editor', status: 'reserved', version: 1, description: 'Draggable editor windows.' },
|
|
86
|
+
{ key: 'insertPalette', side: 'editor', status: 'reserved', version: 1, description: 'Insert-palette categories and entries.' },
|
|
87
|
+
{ key: 'timelineLanes', side: 'editor', status: 'reserved', version: 1, description: 'Extra timeline lanes.' },
|
|
88
|
+
{ key: 'exportSteps', side: 'editor', status: 'reserved', version: 1, description: 'Steps in the export pipeline.' },
|
|
89
|
+
{ key: 'projectMenuItems', side: 'editor', status: 'reserved', version: 1, description: 'Project menu entries.' },
|
|
90
|
+
{ key: 'inspectors', side: 'editor', status: 'reserved', version: 1, description: 'Selected-object inspector sections.' },
|
|
91
|
+
{ key: 'aiSlots', side: 'editor', status: 'reserved', version: 1, description: 'AI slots (roadmap §6).' },
|
|
92
|
+
{ key: 'aiProviders', side: 'editor', status: 'reserved', version: 1, description: 'AI providers (roadmap §6).' },
|
|
93
|
+
];
|
|
94
|
+
const POINT_KEYS = new Set(EXTENSION_POINTS.map((p) => p.key));
|
|
95
|
+
/** `id` grammar: reverse-DNS, ≥ 2 dot-separated labels of `[a-z0-9-]`, each
|
|
96
|
+
* starting with a letter. */
|
|
97
|
+
export const PLUGIN_ID_RE = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
|
98
|
+
export function isPluginId(s) {
|
|
99
|
+
return PLUGIN_ID_RE.test(s);
|
|
100
|
+
}
|
|
101
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
102
|
+
/** Load-time check of a manifest against the host's catalogs, contract version
|
|
103
|
+
* and (optionally) versions. Bundled manifests always pass with no warnings;
|
|
104
|
+
* this is the seam that guards third-party plugins. */
|
|
105
|
+
export function validatePluginManifest(m, opts = {}) {
|
|
106
|
+
const errors = [];
|
|
107
|
+
const warnings = [];
|
|
108
|
+
if (typeof m.id !== 'string' || !isPluginId(m.id))
|
|
109
|
+
errors.push(`invalid plugin id "${String(m.id)}" (expected reverse-DNS, e.g. com.example.myfx)`);
|
|
110
|
+
if (typeof m.name !== 'string' || !m.name)
|
|
111
|
+
errors.push('missing "name"');
|
|
112
|
+
if (typeof m.version !== 'string' || !SEMVER_RE.test(m.version))
|
|
113
|
+
errors.push(`invalid "version" "${String(m.version)}" (expected x.y.z)`);
|
|
114
|
+
if (m.apiVersion !== undefined && m.apiVersion > PLUGIN_API_VERSION) {
|
|
115
|
+
errors.push(`requires plugin API v${m.apiVersion}, host is v${PLUGIN_API_VERSION}`);
|
|
116
|
+
}
|
|
117
|
+
for (const [field, range] of [['engine', m.engine], ['editor', m.editor]]) {
|
|
118
|
+
if (range !== undefined && !isValidRange(range))
|
|
119
|
+
errors.push(`invalid "${field}" range "${range}"`);
|
|
120
|
+
}
|
|
121
|
+
if (m.engine && opts.engineVersion && isValidRange(m.engine) && !satisfiesRange(opts.engineVersion, m.engine)) {
|
|
122
|
+
errors.push(`requires engine ${m.engine}, host is ${opts.engineVersion}`);
|
|
123
|
+
}
|
|
124
|
+
if (m.editor && opts.editorVersion && isValidRange(m.editor) && !satisfiesRange(opts.editorVersion, m.editor)) {
|
|
125
|
+
errors.push(`requires editor ${m.editor}, host is ${opts.editorVersion}`);
|
|
126
|
+
}
|
|
127
|
+
for (const [dep, range] of Object.entries(m.dependencies ?? {})) {
|
|
128
|
+
if (!isPluginId(dep))
|
|
129
|
+
errors.push(`invalid dependency id "${dep}"`);
|
|
130
|
+
if (!isValidRange(range))
|
|
131
|
+
errors.push(`invalid range "${range}" for dependency "${dep}"`);
|
|
132
|
+
}
|
|
133
|
+
for (const p of m.permissions ?? []) {
|
|
134
|
+
const def = matchPermission(p);
|
|
135
|
+
if (!def)
|
|
136
|
+
errors.push(`unknown permission "${p}"`);
|
|
137
|
+
else if (def.status === 'reserved')
|
|
138
|
+
warnings.push(`permission "${p}" is reserved — no host grants it yet`);
|
|
139
|
+
}
|
|
140
|
+
for (const key of Object.keys(m.contributes ?? {})) {
|
|
141
|
+
if (!POINT_KEYS.has(key))
|
|
142
|
+
warnings.push(`unknown extension point "${key}" — ignored`);
|
|
143
|
+
else if (EXTENSION_POINTS.find((p) => p.key === key).status === 'reserved')
|
|
144
|
+
warnings.push(`extension point "${key}" is reserved — ignored`);
|
|
145
|
+
}
|
|
146
|
+
if (m.activation?.engine && !['eager', 'onCommand', 'manual'].includes(m.activation.engine))
|
|
147
|
+
errors.push(`unknown activation.engine "${m.activation.engine}"`);
|
|
148
|
+
if (m.reload && m.reload !== 'hot' && m.reload !== 'restart')
|
|
149
|
+
errors.push(`unknown reload policy "${m.reload}"`);
|
|
150
|
+
if (m.entries && !m.entries.engine && !m.entries.editor)
|
|
151
|
+
warnings.push('"entries" names neither an engine nor an editor module');
|
|
152
|
+
return { errors, warnings };
|
|
153
|
+
}
|
|
154
|
+
/** Convenience: only the errors (the shape most call sites need). */
|
|
155
|
+
export function manifestErrors(m, opts) {
|
|
156
|
+
return validatePluginManifest(m, opts).errors;
|
|
157
|
+
}
|
|
158
|
+
/** Whether the manifest ships a runtime (engine) half. */
|
|
159
|
+
export function hasEngineHalf(m) {
|
|
160
|
+
return !!m.entries?.engine;
|
|
161
|
+
}
|
|
162
|
+
/** Whether the manifest ships an editor half. */
|
|
163
|
+
export function hasEditorHalf(m) {
|
|
164
|
+
return !!m.entries?.editor;
|
|
165
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CommandSchema } from './schema.js';
|
|
2
|
+
import type { PluginManifest } from './plugin-manifest.js';
|
|
3
|
+
/** The reserved id namespace of the first-party plugins (`app.nilvn.textfx`).
|
|
4
|
+
* A plugin under it also answers to its short name everywhere a plugin is named
|
|
5
|
+
* (`[use textfx]`, authoring docs, old projects / packages) — see
|
|
6
|
+
* {@link resolvePluginId}. Third-party ids must be reverse-DNS under their own
|
|
7
|
+
* domain, so a dotless name can only ever mean a first-party short name. */
|
|
8
|
+
export declare const FIRST_PARTY_ID_PREFIX = "app.nilvn.";
|
|
9
|
+
/** A short first-party name → its id (`textfx` → `app.nilvn.textfx`); anything
|
|
10
|
+
* with a dot (already an id) is returned as-is. Pure convention, no table: the
|
|
11
|
+
* engine aliases the same way when it registers a first-party module. */
|
|
12
|
+
export declare function resolvePluginId(nameOrId: string): string;
|
|
13
|
+
/** Whether `id` lives in the first-party namespace (short name or id). */
|
|
14
|
+
export declare function isFirstPartyId(nameOrId: string): boolean;
|
|
15
|
+
/** The last dotted segment of a plugin id (`app.nilvn.textfx` → `textfx`): the
|
|
16
|
+
* first-party plugins' i18n-id prefix (`plugin.textfx.*`) and style-element suffix. */
|
|
17
|
+
export declare function pluginSlug(id: string): string;
|
|
18
|
+
/** The commands a set of manifests contributes, keyed by name. */
|
|
19
|
+
export declare function manifestCommandMap(manifests: readonly PluginManifest[]): Record<string, CommandSchema>;
|
|
20
|
+
/** A serializer / editor command registry: the built-in commands plus every
|
|
21
|
+
* command these manifests contribute. Hosts build it once from the manifests
|
|
22
|
+
* they know (the first-party set and any third-party ones) and pass it to
|
|
23
|
+
* `serializeChunk` / `buildScriptPackage` so plugin commands round-trip with the
|
|
24
|
+
* correct positional / named args. Enabled-agnostic on
|
|
25
|
+
* purpose: round-trip is a document concern, independent of the toggled set. */
|
|
26
|
+
export declare function commandRegistry(manifests: readonly PluginManifest[]): Record<string, CommandSchema>;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Plugin id conventions and manifest-derived registries — the parts of the plugin
|
|
2
|
+
// platform that are CONTRACT, not inventory. Which plugins exist is not core's
|
|
3
|
+
// business: the first-party set (manifests + runtime modules + messages) lives in
|
|
4
|
+
// @nilvn/plugins, third-party ones in their own packages. Core only knows the id
|
|
5
|
+
// grammar, the reserved first-party namespace, and how to derive lookup tables
|
|
6
|
+
// from whatever manifests a host hands it.
|
|
7
|
+
import { BUILTIN_COMMAND_MAP } from './commands.js';
|
|
8
|
+
/** The reserved id namespace of the first-party plugins (`app.nilvn.textfx`).
|
|
9
|
+
* A plugin under it also answers to its short name everywhere a plugin is named
|
|
10
|
+
* (`[use textfx]`, authoring docs, old projects / packages) — see
|
|
11
|
+
* {@link resolvePluginId}. Third-party ids must be reverse-DNS under their own
|
|
12
|
+
* domain, so a dotless name can only ever mean a first-party short name. */
|
|
13
|
+
export const FIRST_PARTY_ID_PREFIX = 'app.nilvn.';
|
|
14
|
+
/** A short first-party name → its id (`textfx` → `app.nilvn.textfx`); anything
|
|
15
|
+
* with a dot (already an id) is returned as-is. Pure convention, no table: the
|
|
16
|
+
* engine aliases the same way when it registers a first-party module. */
|
|
17
|
+
export function resolvePluginId(nameOrId) {
|
|
18
|
+
return nameOrId.includes('.') ? nameOrId : FIRST_PARTY_ID_PREFIX + nameOrId;
|
|
19
|
+
}
|
|
20
|
+
/** Whether `id` lives in the first-party namespace (short name or id). */
|
|
21
|
+
export function isFirstPartyId(nameOrId) {
|
|
22
|
+
return resolvePluginId(nameOrId).startsWith(FIRST_PARTY_ID_PREFIX);
|
|
23
|
+
}
|
|
24
|
+
/** The last dotted segment of a plugin id (`app.nilvn.textfx` → `textfx`): the
|
|
25
|
+
* first-party plugins' i18n-id prefix (`plugin.textfx.*`) and style-element suffix. */
|
|
26
|
+
export function pluginSlug(id) {
|
|
27
|
+
const i = id.lastIndexOf('.');
|
|
28
|
+
return i < 0 ? id : id.slice(i + 1);
|
|
29
|
+
}
|
|
30
|
+
/** The commands a set of manifests contributes, keyed by name. */
|
|
31
|
+
export function manifestCommandMap(manifests) {
|
|
32
|
+
return Object.fromEntries(manifests.flatMap((m) => (m.contributes?.commands ?? []).map((c) => [c.name, c])));
|
|
33
|
+
}
|
|
34
|
+
/** A serializer / editor command registry: the built-in commands plus every
|
|
35
|
+
* command these manifests contribute. Hosts build it once from the manifests
|
|
36
|
+
* they know (the first-party set and any third-party ones) and pass it to
|
|
37
|
+
* `serializeChunk` / `buildScriptPackage` so plugin commands round-trip with the
|
|
38
|
+
* correct positional / named args. Enabled-agnostic on
|
|
39
|
+
* purpose: round-trip is a document concern, independent of the toggled set. */
|
|
40
|
+
export function commandRegistry(manifests) {
|
|
41
|
+
return { ...BUILTIN_COMMAND_MAP, ...manifestCommandMap(manifests) };
|
|
42
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type ParamType = 'string' | 'number' | 'boolean' | 'enum' | 'asset:bg' | 'asset:bgm' | 'asset:se' | 'asset:sprite' | 'actor' | 'face' | 'color' | 'expr';
|
|
2
|
+
export interface ParamOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParamSchema {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
type: ParamType;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
default?: string | number | boolean;
|
|
12
|
+
/** Choices when type === 'enum'. */
|
|
13
|
+
options?: ParamOption[];
|
|
14
|
+
/**
|
|
15
|
+
* When set, this param serializes to a positional DSL argument at the given index
|
|
16
|
+
* (e.g. [char yuki happy] => id at 0, face at 1). Otherwise it serializes as key=value.
|
|
17
|
+
*/
|
|
18
|
+
positional?: number;
|
|
19
|
+
/** Tuck behind an "advanced" disclosure in the editor. */
|
|
20
|
+
advanced?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type CommandCategory = 'stage' | 'audio' | 'fx' | 'flow' | 'text';
|
|
23
|
+
export interface CommandSchema {
|
|
24
|
+
name: string;
|
|
25
|
+
label: string;
|
|
26
|
+
category: CommandCategory;
|
|
27
|
+
/** One-line help shown in the editor. */
|
|
28
|
+
hint?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
params: ParamSchema[];
|
|
31
|
+
}
|
|
32
|
+
export interface TextEffectDef {
|
|
33
|
+
name: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
preview?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Renderable object kind — what the editor needs to label/group stage objects.
|
|
38
|
+
* The transform schema (x/y/scale/rotation/opacity/zIndex/visible) is engine-owned
|
|
39
|
+
* and not mirrored here; the editor only addresses objects, never their transform. */
|
|
40
|
+
export interface ObjectKindSchema {
|
|
41
|
+
id: string;
|
|
42
|
+
/** Display label, a t() key. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** Whether the kind exposes the transform schema (animatable). */
|
|
45
|
+
transformable: boolean;
|
|
46
|
+
/** Emoji shown beside on-stage instances of this kind in the palette. */
|
|
47
|
+
icon?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Built-in kinds, mirroring the engine's BUILTIN_KINDS. `character` is per-actor
|
|
50
|
+
* (the palette shows actor names), `camera`/`screen` are singletons. */
|
|
51
|
+
export declare const BUILTIN_OBJECT_KINDS: ObjectKindSchema[];
|
|
52
|
+
/** A retargetable effect a plugin contributes, bound to kinds via `appliesToKinds`
|
|
53
|
+
* (matching the engine's static binding). Authors never insert an effect directly —
|
|
54
|
+
* they insert the `command` that applies it; this schema tells the editor
|
|
55
|
+
* how to offer the effect for an on-stage object and which CommandNode to produce. */
|
|
56
|
+
export interface EffectSchema {
|
|
57
|
+
name: string;
|
|
58
|
+
/** Display label, a t() key. Used as the menu entry unless `fanout` is set. */
|
|
59
|
+
label: string;
|
|
60
|
+
hint?: string;
|
|
61
|
+
icon?: string;
|
|
62
|
+
/** Kinds this effect can target. */
|
|
63
|
+
appliesToKinds: string[];
|
|
64
|
+
/** The command name that applies this effect (its CommandSchema drives the form). */
|
|
65
|
+
command: string;
|
|
66
|
+
/** Per-kind params written onto the inserted command to point it at the chosen
|
|
67
|
+
* object. The sentinel '$id' is replaced with the object's instance id; a kind
|
|
68
|
+
* absent here (e.g. a singleton with an implicit default) targets implicitly. */
|
|
69
|
+
targetParams?: Record<string, Record<string, string>>;
|
|
70
|
+
/** Name of an enum param to expand into one object-section entry per option
|
|
71
|
+
* (e.g. pose → hop/nod/shake/swing). The chosen value is written to that param. */
|
|
72
|
+
fanout?: string;
|
|
73
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Declarative command & plugin schema — drives the editor UI so built-in commands and
|
|
2
|
+
// third-party plugin commands are edited the same way. Built-in schemas: commands.ts.
|
|
3
|
+
/** Built-in kinds, mirroring the engine's BUILTIN_KINDS. `character` is per-actor
|
|
4
|
+
* (the palette shows actor names), `camera`/`screen` are singletons. */
|
|
5
|
+
export const BUILTIN_OBJECT_KINDS = [
|
|
6
|
+
{ id: 'character', label: 'objectKind.character', transformable: true, icon: '🧍' },
|
|
7
|
+
{ id: 'camera', label: 'objectKind.camera', transformable: true, icon: '🎥' },
|
|
8
|
+
{ id: 'screen', label: 'objectKind.screen', transformable: false, icon: '🖥' },
|
|
9
|
+
// UI windows — today the built-in `window:dialog` singleton (the dialogue box);
|
|
10
|
+
// the future ui-kit plugin contributes more instances through the same kind path.
|
|
11
|
+
{ id: 'window', label: 'objectKind.window', transformable: true, icon: '🪟' },
|
|
12
|
+
];
|
|
13
|
+
// The plugin manifest (v2) and its capability / extension-point catalogs live in
|
|
14
|
+
// plugin-manifest.ts; the command / effect / kind schemas above are what
|
|
15
|
+
// a manifest's `contributes` carries.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ScreenplayFormatOptions {
|
|
2
|
+
/** Target quote style for body text. Default `"` (owner call 2026-07-21);
|
|
3
|
+
* `“”` / `「」` cater to Simplified-Chinese / Japanese typesetting habits. */
|
|
4
|
+
quoteStyle?: '"' | '“”' | '「」';
|
|
5
|
+
/** Target ellipsis form for body text. Default `……` (CJK convention). */
|
|
6
|
+
ellipsis?: '……' | '...';
|
|
7
|
+
}
|
|
8
|
+
export declare function formatScreenplay(text: string, opts?: ScreenplayFormatOptions): string;
|