# Shockless Plugin Authoring
This is the practical guide for creating, installing, validating, and shipping
Shockless plugins.
Shockless plugins are folder-based modules. Each plugin has a manifest named
shockless.plugin.json, an entry file, and optional supporting files. The Plugin
Manager validates the manifest, installs the folder, persists enabled state,
shows the plugin in the rail when enabled, and generates relay packet-hook
policy from enabled plugin permissions.
Built-in app plugins are also folder-based, but they use TypeScript metadata
files instead of user manifests:
src/plugins/<plugin-id>/plugin.ts
The built-in index is src/plugins/builtins.ts; the central
src/plugins/registry.ts only normalizes, sorts, and derives fallback
permissions. Do not add new built-in plugin definition objects directly to
registry.ts.
The portable package also ships readable premade module source folders under:
plugins/_premade-modules/<module-id>/
Each premade module is an installable user-plugin example with an id prefixed
by premade-, for example premade-room. These folders mirror the built-in
module permissions and event/API patterns so plugin authors can inspect real
module-style code without colliding with the native built-in panels.
Current Capability
Usable now:
- Create a plugin from the bundled template.
- Install an existing plugin folder.
- Enable or disable optional plugins.
- Enable or disable each declared UI surface.
- Show user plugins in the plugin rail.
- Show a schema-rendered panel for every plugin. If a plugin omits custom layout JSON, Shockless generates a safe default preview and surface layout from the manifest metadata.
- Persist plugin and surface state.
- Validate plugin folders before install.
- Refuse obvious local private files in plugin folders.
- Run enabled user
plugin.jsentries in a restricted renderer Worker host. - Deliver permission-filtered room, chat, session, runtime, and packet events.
- Provide plugin-scoped storage APIs.
- Provide readable helper groups such as
subscriptions,runtime,room,session.onSelected(), andchat.onMessage(). - Provide room chat helpers through
chat.send(),chat.say(),chat.shout(), andchat.whisper(). - Provide packet-backed avatar controls through
avatar.*, including
coordinate walking and walkToItem() item resolution by id/name/class text.
- Provide packet-backed domain helper APIs through groups such as
social.*,furni.*,wallItems.*, and other typed action namespaces listed in the API reference. - Allow plugins to define local custom events from raw packet observations with
events.defineFromPacket().
- Pass enabled packet permissions into each embedded relay process.
- Filter sensitive client packets from plugin relay policy unless explicitly
granted.
Reserved for later host phases:
- Registering custom console commands from plugin code.
- Blocking, replacing, or mutating live relay traffic from packet event return
values. packets.send() is implemented for explicit validated
client-to-server packet sends.
- Custom command dispatch from registered plugin commands. Manifest command
metadata can be documented today, but runtime registration still returns a
reserved result.
Schema UI is implemented through manifests and restricted runtime updates. Plugin code may update host-rendered schema surfaces with api.ui.registerSurface(), api.ui.updateSurface(), and api.ui.setValue(), then receive interactions through api.ui.onAction(). It cannot mount React components, inject HTML, or access the DOM directly.
Do not build plugins that depend on unrestricted Electron or Node access. User
plugin entries run in a Worker and only receive the restricted api host object passed to activate(api).
Create A Plugin
- Open
Plugin Manager. - Enter a lowercase plugin id, for example
room-tools. - Enter a display name, for example
Room Tools. - Click
Create From Template. - Click
Open Folder. - Edit the created folder.
- Click
Reload Plugins. - Enable the plugin if it is disabled.
Plugin ids must:
- use lowercase letters, numbers, and hyphens
- start with a letter or number
- be 2 to 64 characters
- not match a built-in plugin id
Good ids:
room-toolspacket-notesvisitor-helper
Invalid ids:
Room Toolsroom_toolsplugin!settings
Install A Plugin
Use Install From Folder and select a folder containing
shockless.plugin.json. Shockless validates the selected folder, copies it into the
managed user plugin root, enables it, and reloads the registry.
Install is refused when:
- the manifest is missing
- the manifest is invalid JSON
- the plugin id is invalid
- the plugin id is already used
- the category is unknown
- a permission is unknown
- no surfaces are declared
- the entry path is absolute
- the entry path escapes the plugin folder
- the entry file does not exist
- obvious credential, token, password, webhook, or local test-account files are
present
User plugins are installed under:
%APPDATA%/Shockless/plugins/<plugin-id>/
Portable user plugins can also be placed under:
dist/portable/Shockless/plugins/<plugin-id>/
The portable _premade-modules folder is intentionally ignored by automatic
discovery. Install one of those examples with Plugin Manager -> Install From
Folder when you want to run it.
Folder Layout
Minimum folder:
my-plugin/
shockless.plugin.json
plugin.js
Recommended folder:
my-plugin/
shockless.plugin.json
plugin.js
README.txt
assets/
Do not store account lists, passwords, tokens, webhook URLs, or test
credentials in a plugin folder. The installer rejects obvious private file
names and the package verifier checks source/package-facing files for private
local artifacts.
Packet-facing plugins should prefer named packet helpers and documented event
payloads over numeric header literals. If a header is not known yet, keep that
fact visible in plugin output instead of hardcoding a guessed id; header ids can
move between client builds.
Manifest
Every plugin must include shockless.plugin.json:
{
"id": "sample-plugin",
"name": "Sample Plugin",
"version": "1.0.0",
"author": "local",
"description": "A local Shockless plugin.",
"entry": "plugin.js",
"icon": "terminal",
"category": "developer",
"permissions": ["ui.panel", "packet.read"],
"managedRuntime": {
"clientRights": []
},
"surfaces": [
{
"id": "panel",
"kind": "panel",
"label": "Sample Panel",
"enabledByDefault": true,
"summary": "Starter panel for a local plugin."
}
],
"commands": [],
"hotkeys": []
}
Required fields:
id: stable plugin id.name: user-visible name.version: plugin version string.entry: relative path to the entry JavaScript file.category: user plugin category.permissions: permission list.surfaces: at least one UI surface. Surfaces may include alayoutarray; if omitted, Shockless generates a metadata-based schema layout so the plugin still has a reproducible panel.
Optional fields:
author: author name.description: short panel/manager summary.icon: icon key from the app icon map.managedRuntime.clientRights: source Session rights the plugin owns and
should clean up when disabled. This requires the client.rights permission.
commands: reserved command declarations.hotkeys: reserved hotkey declarations.
Categories
Allowed user categories:
session: multi-client/session tools.room: room data, room actions, navigation, overlays.user: avatar/user tools.inventory: hand/items/inventory helpers.automation: controlled repeated workflows.social: friends, messages, requests, visitor-related tools.developer: diagnostics, packet tooling, experimental local tools.
Reserved category:
core: built-in recovery tabs only.
Icons
Use one of the existing icon keys. Unknown keys fall back to an alert icon.
These keys map to the app's bundled Lucide icon set and are the only stable
manifest values today:
activity: performance, status, or live tracking.bot: automation, session, or repeated workflow helpers.command: commands, mapped actions, or quick tools.hammer: builder, wall, furniture, or room editing.info: facts, lookups, account data, and summaries.list: logs, packet tables, history, and ordered data.map: rooms, Navigator, location, and route tools.messages: chat, social, private messages, and requests.package: inventory, hand, item bundles, or collected objects.plug: connection, profile import, lifecycle, and relay status.sofa: room furniture and room object tools.terminal: console, packet, diagnostics, and development tools.user: avatar/user profile and user action tools.wrench: settings, utilities, and repair tools.
Surfaces
Each plugin needs at least one surface:
{
"id": "panel",
"kind": "panel",
"label": "Sample Panel",
"enabledByDefault": true,
"summary": "Starter panel for a local plugin."
}
Fields:
id: surface id inside the plugin.kind:panel,overlay,status, orcommands.label: user-visible surface name.enabledByDefault: whether the surface starts enabled.summary: short user-visible explanation.
Current behavior:
- Plugin enabled state controls whether the plugin runs and appears in the app
rail. Disabling a plugin stops its Worker, blocks normal runtime API calls,
and keeps only storage/declared cleanup routes available during disposal.
panel: controls whether the host-rendered panel surface is mounted in the
Plugin Manager detail page.
overlay: controls overlay-style surfaces. Built-in overlays such as Room
Info and Social private-message bulletins are gated by their own overlay
surfaces; user plugin overlays are persisted for schema/runtime use.
status: controls status-style surfaces. Built-in status surfaces such as the
FPS overlay are gated by this setting; user plugin status surfaces are
persisted for schema/runtime use.
commands: controls whether the Plugin Manager shows that plugin's
command/hotkey metadata tab. Runtime plugin command registration remains a
reserved host phase.
The Plugin Manager can toggle each surface independently.
Schema UI
Plugin UI is declarative. Plugin code does not register React components or
write HTML into the app. The manager renders JSON schema elements from
ui.preview, ui.settings, and surfaces[].layout with the same controls used
by built-in plugins. This keeps third-party plugin UI consistent and makes a
plugin reproducible from its manifest plus plugin.js.
Supported schema element types:
header: section heading.text: plain explanatory text.divider: horizontal separator.section: nested group of schema elements.notice: themed message block.button: action or command trigger.buttonGrid: responsive group of action buttons with one to six columns.toggle/checkbox: boolean setting.textInput,numberInput,slider,colorInput,select,keybind:
editable controls.
table: structured rows. OptionalrowKey,selectedRowKey, and
rowAction make rows selectable and emit ui.onAction() events with the
selected row key. Optional maxRows limits rendered rows without truncating
the source data in plugin code.
kv: key/value facts.log: fixed log-style rows.
Every action element may declare action; buttons may also declare command.
The runtime host delivers control changes through api.ui.onAction() and the
renderer routes built-in action strings through the app command handlers.
Schema control values are persisted by plugin id and element id in local app
storage so plugin settings survive app restarts. Plugins that need structured
or long-lived data should still use the plugin-scoped api.storage calls.
Runtime schema helpers:
api.ui.registerSurface({ id, layout }): creates or replaces a host-rendered schema surface for this plugin.registerPanel()is a compatibility alias for the same schema path, not a React panel hook.api.ui.updateSurface(surfaceId, layout): replaces the layout for a declared surface such aspanel.api.ui.setValue(key, value): updates the current value shown by a schema control.api.ui.onAction(handler): subscribes to button, button-grid, table row,
toggle, checkbox, input, select, and keybind events emitted by the schema
renderer.
Plugin UI must stay declarative JSON schema. Arbitrary HTML, DOM access, and React component mounting are intentionally blocked.
Example selectable table plus action grid:
{
"type": "table",
"label": "Room Items",
"columns": [
{ "key": "name", "label": "Item" },
{ "key": "id", "label": "ID" }
],
"rows": [
{ "key": "floor:123", "name": "Chair", "id": 123 }
],
"rowKey": "key",
"selectedRowKey": "floor:123",
"rowAction": "items.select",
"maxRows": 80
}
{
"type": "buttonGrid",
"columns": 3,
"buttons": [
{ "label": "Use", "action": "items.use", "variant": "primary" },
{ "label": "Rotate", "action": "items.rotate" },
{ "label": "Pickup", "action": "items.pickup", "variant": "danger" }
]
}
Permissions
Only request what the plugin actually needs.
ui.panel: plugin declares a panel surface.ui.status: plugin declares a status surface.ui.overlay: plugin declares an overlay surface.console.commands: plugin intends to register console commands.engine.snapshot: plugin needs selected-client runtime snapshots.engine.control: plugin intends to control engine/runtime behavior.notifications.show: plugin can render selected-client native top-right
bulletin notifications through the Shockless notification manager.
client.rights: plugin reads or mutates selected-client source Session
rights such as fuse_habbo_chooser and fuse_furni_chooser.
events.room: plugin listens to room lifecycle/user events.events.chat: plugin listens to chat events.events.packet: plugin listens to packet events without packet mutation.events.session: plugin listens to client/session events.chat.send: plugin sends normal room chat.chat.say,chat.shout, andchat.whisperare readable mode helpers over the same permission.actions.avatar: plugin sends validated avatar movement/action packets such
as walk, wave, dance, carry drink, and apply look.
actions.social: plugin sends validated social packets such as friend
request, private message, accept/decline request, remove, follow, and request
refresh.
actions.fishing: plugin sends validated Fishing start, minigame input,
derby register, token, products, rod, stats, and Fishopedia packets. Use it
with actions.avatar when the plugin also needs generic movement such as
avatar.walkToArea().
actions.furni: plugin sends validated floor/wall furniture move, rotate,
use, and pickup packets through furni.*.
actions.plants: plugin sends validated Gardening plant move, water,
harvest, and compost packets. Pair it with engine.snapshot for
plants.findPlants(), plants.getState(), plants.planCycle(), and
plants.runCycle().
actions.wallItems: plugin sends validated wall item move and pickup
packets.
storage: plugin needs plugin-scoped persistence.packet.read: plugin needs non-sensitive decrypted packet metadata.packet.inject: plugin intends to send scoped packets.packet.intercept: plugin intends to intercept non-sensitive decrypted
packets.
packet.intercept.sensitive: plugin explicitly needs sensitive packet
families.
Sensitive access is never implied by packet.read or packet.intercept.
Sensitive client headers currently include login/key identity families such as
TRY_LOGIN [4], UNIQUEID [6], and GENERATEKEY [202].
The client.*Rights APIs mutate the selected Shockless source
Session.user_rights list for advanced clientside runtime experiments. They do
not grant server account privileges and no bundled public addon enables them by
default.
If a plugin grants source Session rights, list them in
managedRuntime.clientRights. Disabled plugins are blocked from normal runtime
actions, but their dispose handler may still remove only those declared rights.
Entry File
The template entry uses an ESM-style module:
export function activate(api) {
const { log, packets, subscriptions } = api;
const cleanup = subscriptions.create();
log.info("Sample plugin activated.");
cleanup.add(packets.on("server", { header: 24 }, (packet) => {
log.info(`server packet ${packet.header}`);
return packet.allow();
}));
return cleanup.dispose;
}
Current status:
- The entry path is validated and displayed.
- The entry file is packaged/copied with the plugin.
- Enabled user plugin entries are loaded in a restricted Worker host.
- The host enforces manifest permissions before delivering events or running
privileged APIs.
Preferred plugin code destructures the API groups it uses:
export async function activate(api) {
const { avatar, log, room, storage, subscriptions } = api;
const cleanup = subscriptions.create();
await storage.remember("lastActivated", { active: true });
cleanup.add(room.onReady((event) => log.info(`Room ready: ${event.room?.name ?? "unknown"}`)));
await avatar.wave();
return cleanup.dispose;
}
The complete source-backed API list lives in
plugin-api.html. Keep examples in this
guide on the clean group style: avatar.walkToItem(), furni.moveFloorItem(), chat.say(), packets.on(), and so on.
The architecture direction behind this style is documented in
xabbo-style-plugin-rewrite-plan.md and
plugin architecture notes.
Origins/Shockwave packet builders and runtime managers.
Common events are available through raw events.on(...) and, where listed, through readable helper groups such as room.onReady(...), runtime.onSnapshot(...), session.onSelected(...), and chat.onMessage(...).
The host also provides small script helpers so plugins do not copy parser
boilerplate into every file:
storage.remember(key, value): stores{ value, updatedAt }.room.itemKey(item),room.userKey(user), androom.objectId(item):
stable ids for live maps and action routing.
room.summarizeItem(item),room.summarizeItems(items), and
room.countItems(event): compact data for UI/storage.
room.keepSelectedItem(selected, items)and
room.keepSelectedWallItem(selected, items): preserve selected items across
room updates.
furni.wallMoveLocation(item, { deltaX, deltaY, orientation }): converts a
parsed wall item into the move shape accepted by furni.moveWallItem().
packets.summary(packet),packets.hasName(packet, ...names), and
packets.field(packet, labelOrRegex): readable packet checks without local
packet-summary helper functions.
Common events:
session.selected: selected client changed.runtime.snapshot: selected visible runtime snapshot changed.room.changed: selected visible client entered or changed room.room.ready: selected visible room became ready.room.users: full current room user list after a room change.room.userJoined: a new room user appeared after the current room snapshot.room.userLeft: a room user disappeared after the current room snapshot.room.items: full parsed floor and wall item list after a room load or item
change.
room.floorItemsLoaded/room.wallItemsLoaded: floor/wall item lists after
a room load.
room.itemAdded/room.itemUpdated/room.itemRemoved: item deltas for
all room object kinds, with floor/wall-specific variants.
chat.message: a new selected-room chat line appeared.packet: new decoded packet row from any direction.packet.client: new client-to-server decoded packet row.packet.server: new server-to-client decoded packet row.
Packet event filtering:
const { log, packets } = api;
const off = packets.on("server", { header: 24 }, (packet) => {
log.info(JSON.stringify(packets.summary(packet)));
return packet.allow();
});
Supported directions are server, client, and all. Filters can include
header or packetName. Packet handler return values are accepted for API
compatibility, but packet mutation is not applied until validated mutation is
enabled.
Raw sends are available through packets.send(clientId, packet) when the
manifest includes packet.inject. Prefer typed APIs for known actions, but use
the raw builder when you are testing a valid client packet family that does not
have a domain helper yet:
export async function activate({ log, packets, session }) {
const clients = await session.getClients();
const clientId = clients.selectedClientId;
await packets.send(clientId, { packetName: "WAVE" });
await packets.send(clientId, { header: 52, bodyEscapedText: "hello[0]" });
log.info("Sent two validated client packets.");
}
The builder accepts one body source per packet: bodyBytes, bodyHex,
bodyText, bodyEscapedText, or full packetText. packetName must resolve
through the outgoing packet table. Unknown numeric headers are allowed and
display as UNKNOWN_HEADER; sensitive login/key headers such as
TRY_LOGIN [4], UNIQUEID [6], and GENERATEKEY [202] are refused.
Plugins can schedule the same validated API without private host access. Keep
the timer owned by the plugin cleanup scope so disabling or unloading the
plugin stops future sends:
export async function activate({ packets, session, subscriptions }) {
const cleanup = subscriptions.create();
const timer = setInterval(async () => {
const clients = await session.getClients();
await packets.send(clients.selectedClientId, { packetName: "WAVE" });
}, 60_000);
cleanup.add(() => clearInterval(timer));
return cleanup.dispose;
}
The built-in Injection panel uses this same validated builder. Its panel is
schema-rendered and contains only packet direction, complete packet text,
validation, session/repeat targeting, saved packets, and sent history. Source
window IDs, element IDs, stage clicks, and other runtime commands belong to
their domain APIs rather than packet injection.
Action Examples
Walk to a tile, parsed floor item, parsed area, or live room user:
export async function activate(api) {
const { avatar } = api;
await avatar.walkTo(6, 8, 0, { clientId: 1 });
await avatar.walkToItem("pumpkin", { clientId: 1 });
await avatar.walkToItem({ objectId: 12345 }, { clientId: 1 });
await avatar.walkToArea({ className: "ads_fish_area", exact: true }, { clientId: 1 });
await avatar.walkToArea({ x: 10, y: 12 }, { clientId: 1 });
await avatar.walkToUser("dek", { offset: { x: 1, y: 0 }, clientId: 1 });
}
walkToItem() and object-selector walkToArea() resolve only floor/passive
objects with tile coordinates. walkToUser() resolves the live room user list by
name, account id, room index, or row id. Use furni.* for moving, rotating, or
picking up furniture, and teleport.enter() for the common walk-and-use
teleport flow.
Find, move, rotate, use, or pick up furniture with selectors:
export async function activate(api) {
const { furni, log, room, subscriptions } = api;
const cleanup = subscriptions.create();
cleanup.add(room.onItems(async (event) => {
const pumpkins = event.floorItems.filter((item) => String(item.searchText || item.name || item.className).includes("pumpkin"));
log.info(`pumpkins in room: ${pumpkins.length}`);
}));
const matching = await furni.findItems({ query: "chair", kind: "floor" });
for (const item of matching) {
await furni.rotateFloorItem(item, 4);
}
return cleanup.dispose;
}
For pickup scripts, pass a room item payload or include kind so the host knows
whether to send the floor or wall pickup route:
const posters = await furni.findItems({ query: "poster", kind: "wall" });
for (const poster of posters) await furni.pickupItem(poster);
const poster = await furni.findItem({ query: "poster", kind: "wall" });
const nextLocation = furni.wallMoveLocation(poster, { deltaX: 1 });
if (nextLocation) await furni.moveWallItem(nextLocation, nextLocation);
Use a floor item by id or selector:
export async function activate({ avatar, furni }) {
const item = await furni.findItem({ query: "switch", kind: "floor" });
if (!item?.tile) return;
await avatar.walkTo(item.tile.x, item.tile.y, Number(item.objectId));
await furni.useFloorItem(item, "0");
}
furni.useFloorItem() sends SET_STUFF_DATA [74] with the selected object id
and value as outgoing Shockwave strings. Use it for floor furniture that accepts
state through the standard item-use route.
Allow native floor item drag/drop outside visible room tiles:
export async function activate({ furni }) {
await furni.setAnywherePlacementEnabled(true);
return () => furni.setAnywherePlacementEnabled(false);
}
furni.setAnywherePlacementEnabled() requires actions.furni and
engine.control. It keeps the normal Object Mover cursor and click flow in
charge; it only changes the floor-coordinate validation while the plugin is
enabled. When that source mover commits a synthetic off-room coordinate,
Shockless sends the same ORIGINS_SET_FURNI_LOCATION [1257] packet used by
Origins' precise mover instead of the model-bounded MOVESTUFF [73] route.
Use the precise floor-location route directly when a plugin already knows the
object id and target coordinate:
export async function activate({ furni }) {
const item = await furni.findItem({ query: "trophy", kind: "floor" });
if (!item) return;
await furni.setFloorItemLocation(item, 14, 11, 0);
}
furni.setFloorItemLocation() requires actions.furni. Selectors still need
engine.snapshot; numeric ids with explicit x/y/height can be sent without a
snapshot.
Leave a room when a matching user or badge appears:
export function activate({ room, rooms, subscriptions }) {
const cleanup = subscriptions.create();
cleanup.add(room.onUserJoined(async ({ user, clientId }) => {
if (user.name === "SomeUser" || user.badgeCode === "BADGE_CODE") {
await rooms.leave({ clientId });
}
}));
return cleanup.dispose;
}
Use a teleport item without writing packet code:
export async function activate({ teleport }) {
await teleport.enter({ query: "teleport", kind: "floor" });
}
Allow native wall item drag/drop outside visible wall bounds:
export async function activate({ wallItems }) {
await wallItems.setAnywherePlacementEnabled(true);
return () => wallItems.setAnywherePlacementEnabled(false);
}
Move a wall item with parsed wall/local coordinates:
await wallItems.moveItem({
itemId: 12345,
wallX: 3,
wallY: 1,
localX: 24,
localY: 12,
orientation: "l",
className: "poster",
});
Move a wall item to a point the user clicks on the stage:
export async function activate({ engine, room, stage, wallItems }) {
const snapshot = await engine.getSnapshot();
const item = room.wallItemsFromSnapshot(snapshot)[0];
if (!item) return;
const point = await stage.captureNextClick({ timeoutMs: 15000 });
await wallItems.moveAnywhere(item, point);
}
Hide selected users locally while the plugin is active:
export async function activate({ filters }) {
await filters.setHiddenUsers(["SomeUser", "233421"]);
return () => filters.clearHiddenUsers();
}
Define plugin-local events from packet data when the host does not expose a
named event yet:
export function activate(api) {
const { events, log } = api;
const offPacket = events.defineFromPacket(
"custom.usersPacket",
"server",
{ packetName: "USERS" },
(packet) => ({ count: packet.decodedFields.length, packet }),
);
const offEvent = events.on("custom.usersPacket", ({ count }) => {
log.info(`USERS packet decoded fields: ${count}`);
});
return () => {
offPacket();
offEvent();
};
}
Packet Hooks
Packet-hook policy is generated from enabled plugin permissions and passed to
each embedded relay before the client starts.
Relay evaluation point:
- client to server: after browser-side plaintext is parsed, before writing to
the official TCP socket
- server to client: after official plaintext is parsed, before writing to the
browser websocket
Policy decisions include:
- whether the packet is sensitive
- which enabled plugins can read it
- which enabled plugins can intercept it
- which enabled plugins can inject scoped packets
Current behavior:
- The relay policy is generated from enabled plugin permissions and is passed
to each relay process.
- The renderer host emits decoded packet rows as
packet,packet.client,
and packet.server events to plugins with packet/event permissions.
- Packet handler return values such as
allow,block,replace, and
inject are accepted by the worker API for forward compatibility.
- Packet mutation is not applied yet. Raw packet send/mutation will only be
enabled through a validated Shockwave packet builder so plugins cannot send
malformed packets.
This follows the same rule as mature proxy extension systems: broad hooks are
useful, but packet writes need type/direction validation before they touch a
live session.
Commands And Hotkeys
commands and hotkeys are manifest fields reserved for plugin command
registration. They are validated as arrays but not executed yet. Plugin code
can call console.registerCommand(), but the host returns a reserved feature
result until the command registry phase is implemented.
Recommended future shape:
{
"commands": [
{
"name": "sample",
"label": "Sample",
"description": "Run sample command."
}
],
"hotkeys": [
{
"key": "F8",
"command": "sample"
}
]
}
For built-in command bindings today, use the Settings tab or the backtick
console bind command.
Command name values are machine ids and must be lowercase identifier-style
strings such as sample.ping. Use optional label for display text in Plugin
Manager.
Plugin Manager UI
Plugin Manager shows:
- user plugin root
- portable plugin root
- total plugin count
- enabled plugin count
- create-from-template controls
- install/reload/open-folder actions
- every built-in and user plugin
- pinned/enabled/disabled state
- permissions
- surfaces and surface toggles
- load errors
Pinned plugins cannot be disabled. Currently pinned core plugins are Plugin
Manager and Settings. Connection starts enabled by default but remains an
optional module.
User Plugin Panel
Every enabled user plugin gets a standard panel showing:
- status
- version
- author
- category
- entry file
- surface count
- permissions
- surface enabled state
- open-folder action
- reload action
This gives users immediate feedback that a created or installed plugin is
registered correctly even before custom panel rendering is available.
Packaging
Portable builds include:
resources/app/dist/plugins/template/shockless.plugin.json
resources/app/dist/plugins/template/plugin.js
resources/app/dist/plugins/template/README.txt
plugins/_premade-modules/README.txt
plugins/_premade-modules/welcome-message/shockless.plugin.json
plugins/_premade-modules/welcome-message/plugin.js
plugins/_premade-modules/welcome-message/README.txt
plugins/_premade-modules/<module-id>/shockless.plugin.json
plugins/_premade-modules/<module-id>/plugin.js
plugins/_premade-modules/<module-id>/README.txt
The portable verifier fails if the template files, premade Welcome Message
plugin, or premade module source pack are missing. Premade source-reference
plugins live in Shockless/plugins/_premade-modules/ and can be installed
through Plugin Manager when users want to copy or customize them.
Portable users can install plugins through the manager or place plugin folders
under:
Shockless/plugins/<plugin-id>/
Validation Checklist
When developing plugin-manager changes:
npm run typecheck -- --pretty false
npm test
npm run build
Relevant tests:
tests/pluginManager.test.tstests/pluginRelayHooks.test.tstests/contracts.test.tstests/securityArtifacts.test.ts
Useful manual checks:
- Create a plugin from template.
- Reload plugins.
- Disable the created plugin and confirm it disappears from the rail.
- Re-enable it from Plugin Manager and confirm the user plugin panel opens.
- Install a copied plugin folder and confirm duplicate ids are rejected.
- Try installing a folder with an obvious private file name and confirm it is
refused.
Example Plugins
Working welcome message example:
examples/premade-plugins/welcome-message/
shockless.plugin.json
plugin.js
README.txt
What it demonstrates:
- manifest permissions for
events.room,chat.send,storage, and
engine.snapshot
events.on("room.userJoined", handler)- ignoring the selected user through the
event.user.isSelfflag - a per-room/user cooldown using plugin-local memory
- room chat sending through
chat.say(message, { clientId }),chat.shout(), orchat.whisper()
Install it with Plugin Manager -> Install From Folder, select
examples/premade-plugins/welcome-message, then enter a private room and let another
user join.
Read-only packet watcher manifest:
{
"id": "packet-watcher",
"name": "Packet Watcher",
"version": "1.0.0",
"entry": "plugin.js",
"category": "developer",
"permissions": ["ui.panel", "packet.read"],
"surfaces": [
{
"id": "panel",
"kind": "panel",
"label": "Packet Watcher",
"enabledByDefault": true,
"summary": "Displays packet watcher status."
}
]
}
Room overlay manifest:
{
"id": "room-overlay-tools",
"name": "Room Overlay Tools",
"version": "1.0.0",
"entry": "plugin.js",
"category": "room",
"permissions": ["ui.panel", "ui.overlay", "engine.snapshot"],
"surfaces": [
{
"id": "panel",
"kind": "panel",
"label": "Room Overlay Tools",
"enabledByDefault": true,
"summary": "Room overlay controls."
},
{
"id": "overlay",
"kind": "overlay",
"label": "Room Overlay",
"enabledByDefault": false,
"summary": "Optional room overlay surface."
}
]
}
Room action manifest:
{
"id": "room-action-tools",
"name": "Room Action Tools",
"version": "1.0.0",
"entry": "plugin.js",
"category": "automation",
"permissions": ["ui.panel", "engine.snapshot", "actions.avatar", "actions.furni"],
"surfaces": [
{
"id": "panel",
"kind": "panel",
"label": "Room Actions",
"enabledByDefault": true,
"summary": "Schema controls for selected room actions."
}
]
}