Plugin API reference (nparseplus_sdk 1.x)¶
Everything importable from the nparseplus_sdk package root is the public
contract and follows semantic versioning: breaking changes only in a new
SDK major, and the app refuses plugins whose requires_sdk range does not
admit the SDK it bundles. SDK_VERSION is the installed SDK's version
string — a plain literal in nparseplus_sdk/__init__.py, deliberately not
an importlib.metadata lookup (a frozen app has no dist metadata).
PluginMeta¶
Frozen pydantic model — your plugin's identity and compatibility claim.
| Field | Type / default | Meaning |
|---|---|---|
id |
str, required |
^[a-z][a-z0-9_-]{1,39}$; keys consent, storage, window ids |
name |
str, required |
display name |
version |
"0.0.0" |
your plugin's own version |
requires_sdk |
">=1.0,<2" |
PEP 440 range vs the bundled SDK |
min_app_version |
None |
optional minimum nParse+ version |
description / author / homepage |
"" |
shown in consent + manager UI |
update_url |
"" |
optional https index the app polls for your updates — see Shipping updates |
check_compat(meta, *, sdk_version, app_version=None) -> str | None returns
the human-readable refusal reason, or None when loadable. sdk_version is
keyword-only and required; malformed version strings come back as a reason,
never an exception.
NParsePlugin¶
Base class. Subclass, set meta as a class attribute, implement
activate(ctx); deactivate() is optional (app shutdown, best-effort).
Expose create_plugin() at module level returning an instance.
PluginContext¶
The capability object handed to activate (a typing.Protocol; the app
implements it in nparseplus.core.plugins.context.HostPluginContext,
nparseplus_sdk.testing.FakePluginContext fakes it).
Identity / environment
| Member | Meaning |
|---|---|
meta |
your validated PluginMeta |
app_version / sdk_version |
host versions, as strings |
logger |
logging.Logger named nparseplus.plugins.<your-id> (lands in nparseplus.log) |
storage |
per-plugin persistence (below) |
Backend access (driver-thread objects — touch only inside your subscriptions/ticks)
The SDK types these as Any on purpose: they are host objects, and typing
them would give the SDK a hard dependency on the app. The concrete classes,
for when you need to read the source or set up type checking with the app
installed:
| Member | Concrete type | Meaning |
|---|---|---|
timers |
nparseplus.core.timers.TimersService |
the spell-timer window's row store; row classes via nparseplus_sdk.timers |
player |
nparseplus.core.player.ActivePlayer |
the active character (name, server, player_class, …) — read-only by convention |
speaker |
satisfies nparseplus.audio.tts.Speaker |
text-to-speech: speak(text). The app hands you a swappable holder, so voice/volume changes follow automatically |
pigparse |
nparseplus.net.pigparse_api.PigParseApiClient, satisfying the Qt-free nparseplus.core.pigparse.PigParseApi protocol |
PigParse REST client (item_prices, item_wiki, boat_activity, …). Reading the property is thread-safe; its methods block on HTTP, so call them only inside a submit fetch |
With sharing off, the host lazily builds one PigParse client and one network
worker shared by all plugins, so ctx.pigparse and ctx.submit are never
None and never depend on the user's sharing settings.
Registration (call during activate)
| Method | Contract |
|---|---|
subscribe(EventClass, fn) -> Unsubscribe |
exact-type dispatch; fn runs on the driver thread, exceptions contained. Subscribe-only — there is no publish |
add_parser(parser) |
parser.handle(line, ctx) -> bool (True = consume); appended after all built-ins, so it never sees a consumed line |
add_tick(fn) |
fn(now: datetime) every ~100 ms on the driver thread. Supervised: two consecutive runs over 250 ms and the driver drops it permanently — see the tick budget |
submit(fetch, apply=None) |
fetch() on a worker thread; apply(result) back on the driver thread. A raise in fetch is logged and drops the apply |
add_window(PluginWindowSpec) |
declare an overlay window |
add_settings_page(PluginSettingsPageSpec) |
declare a Settings page |
PluginStorage¶
ctx.storage — isolated from the app's settings, living in
plugin-data/<id>/ under the config directory:
load() -> dict— the plugin's JSON store (missing/corrupt →{})save(dict)— atomic write (tmp + rename)data_dir -> Path— a private directory for anything bigger
Both the store and the directory are moved to plugins/trash/plugin-data/
when the user uninstalls the plugin.
Window & settings-page specs¶
PluginWindowSpec(key, title, factory, default_geometry=(200,200,320,240),
command_key=None) — key must match the plugin-id pattern and be unique
within your plugin; declare it twice and only the first window is kept — the
second would share the first's window_key, so the app logs a warning and
drops it (tray entry, chat toggle and all).
factory(wctx) runs on the GUI thread and returns any widget with
.toggle()/.isVisible(); subclassing nparseplus_sdk.ui.PluginWindow is
the recommended way (overlay recipe + persistence for free — call
self.restore_visibility() last, and only such a window gets a
Settings → Windows row). The in-game
chat toggle is toggle_<command_key> (default <id>_<key>, with any
non-word character mapped to _). title is user-facing in three places:
the tray entry, the Settings → Windows row (prefixed with your meta.name),
and the window's own title bar.
PluginWindow.__init__(wctx, *, translucent=True, default_state=None,
parent=None) — the keyword arguments are passed through to
OverlayWindowBase; self.window_context holds the wctx you were given.
PluginWindowContext (the wctx your factory receives) — a dataclass
with six fields plus one extension point:
| Field | Meaning |
|---|---|
settings |
the host's pydantic Settings root |
window_key |
this window's canonical key, plugin.<id>.<spec key> — Settings.windows[window_key] is the state the user edits in Settings → Windows |
title |
the spec's title |
default_geometry |
the spec's (x, y, w, h) |
on_save |
call to request a settings save |
bridge |
the Qt bridge whose event_received / events_batch signals deliver bus events on the GUI thread (None outside the app) |
extras |
dict[str, Any], empty today — a forward-compatibility slot; do not rely on any key |
PluginSettingsPageSpec(title, builder, apply=None) — builder(parent)
-> QWidget builds the page; apply(widget) runs on Settings
"Apply && Save". Both are individually guarded by the app.
Host re-export modules (lazy)¶
These import the running app on first attribute access, so importing your plugin stays possible in Qt-free/host-free environments:
nparseplus_sdk.events— the typed event catalogue (LineEvent— the every-line firehose,CommsEvent+CommsChannel,YouZonedEvent,DeathEvent, …), forwarded fromnparseplus.core.events. Subscribe with the exact class.nparseplus_sdk.timers—TimerRow,CounterRow,SpellRow,RollRowand group constants, forwarded fromnparseplus.core.timers.nparseplus_sdk.ui—PluginWindow, forwarded fromnparseplus.ui.pluginwindow(needs PySide6; keep it out of your plugin's top-level module — see Windows).
Outside the app these raise ImportError with a message telling you to
install nparseplus from source.
Testing & validation helpers¶
nparseplus_sdk.testing.FakePluginContext(meta=None, *, app_version, sdk_version, storage, timers, player)— recordssubscriptions/parsers/ticks/windows/settings_pages/submitted;publish(event)drives subscriptions by exact type,run_submitted()executes and clears queued fetch/apply pairs; fakestorage(FakeStorage, with.dataand.save_count),speaker(.spoken), andpigparse(RecordingApi, with.calls).nparseplus_sdk.validate.validate_plugin(path, *, app_version=None) -> ValidationReport— the engine behind thenparseplus-plugin validateCLI.app_versionis keyword-only. The report carriesok,errors,warnings,meta, and the registration counts (window_count,page_count,parser_count,subscription_count,tick_count). Warnings are advisory only and never affectok. Note that validating imports the plugin and callsactivate().
Other exports¶
LineParser, LineInfoLike, Speaker, PluginStorage (protocols),
Unsubscribe (type alias), PLUGIN_ID_RE.