Skip to content

Plugin Structure

A complete reference for plugin file organization.

Folder Location

Plugins live in ~/.gemshell/plugins/:

~/.gemshell/plugins/
├── my-plugin/
│   ├── plugin.js       # Manifest (required)
│   ├── style.css       # Styles (optional)
│   ├── template.html   # HTML template (optional)
│   └── script.js       # JavaScript (optional)
└── another-plugin/
    └── ...

plugin.js (Required)

The manifest file defines your plugin:

javascript
module.exports = {
  // Required
  name: 'My Plugin',
  version: '1.0.0',
  
  // Optional metadata
  author: 'Your Name',
  description: 'What your plugin does',
  gemshell: '>=0.6.0',  // Minimum GemShell version
  
  // Files to inject into the game
  inject: ['style.css', 'template.html', 'script.js'],
  
  // User-configurable settings
  settings: [
    { id: 'enabled', type: 'checkbox', label: 'Enable', default: true }
  ],
  
  // Build hooks (optional)
  async onPreBuild(context, settings) { },
  async onModifyAssets(context, settings) { },
  async onBeforePackage(context, settings) { },
  async onPostBuild(context, settings) { },
  async onBuildError(context, settings) { }
};

style.css (Optional)

CSS styles injected into <head>:

css
.my-plugin-element {
  position: fixed;
  z-index: 99999;
  /* ... */
}

template.html (Optional)

HTML with template syntax, injected into <body>:

html
{{#if enabled}}
<div class="my-plugin-element" id="my-element">
  {{message}}
</div>
{{/if}}

script.js (Optional)

JavaScript injected at end of <body>:

javascript
// GEMSHELL_SETTINGS is auto-injected with setting values
const { enabled, message } = GEMSHELL_SETTINGS;

if (!enabled) return;

// Your code here
const el = document.getElementById('my-element');

Injection Order

Files are injected in this order:

  1. CSS into <head> (styles load first)
  2. HTML into <body> (DOM elements created)
  3. JS into <body> (can access elements)

File Naming

You can name files anything, just reference them in inject:

javascript
inject: ['styles/main.css', 'ui/overlay.html', 'src/app.js']

Paths are relative to your plugin folder.

Minimal Plugin

The simplest plugin needs only plugin.js:

javascript
module.exports = {
  name: 'Hello',
  version: '1.0.0',
  
  async onPreBuild(context, settings) {
    console.log('Building:', context.config.title);
  }
};

Full Example

my-fps-counter/
├── plugin.js
├── style.css
├── template.html
└── script.js

plugin.js:

javascript
module.exports = {
  name: 'FPS Counter',
  version: '1.0.0',
  author: 'Developer',
  description: 'Displays FPS in-game',
  gemshell: '>=0.6.0',
  
  inject: ['style.css', 'template.html', 'script.js'],
  
  settings: [
    { id: 'enabled', type: 'checkbox', label: 'Show FPS', default: true },
    { id: 'position', type: 'select', label: 'Position', 
      options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
      default: 'top-left' }
  ]
};

style.css:

css
.fps-counter {
  position: fixed;
  padding: 4px 8px;
  background: rgba(0, 0, 0, 0.7);
  color: #4ade80;
  font-family: monospace;
  font-size: 14px;
  z-index: 99999;
}
.fps-counter.top-left { top: 10px; left: 10px; }
.fps-counter.top-right { top: 10px; right: 10px; }
.fps-counter.bottom-left { bottom: 10px; left: 10px; }
.fps-counter.bottom-right { bottom: 10px; right: 10px; }

template.html:

html
{{#if enabled}}
<div class="fps-counter {{position}}" id="fps-display">-- FPS</div>
{{/if}}

script.js:

javascript
const { enabled } = GEMSHELL_SETTINGS;
if (!enabled) return;

const display = document.getElementById('fps-display');
let frames = 0, lastTime = performance.now();

function update() {
  frames++;
  const now = performance.now();
  if (now - lastTime >= 1000) {
    display.textContent = frames + ' FPS';
    frames = 0;
    lastTime = now;
  }
  requestAnimationFrame(update);
}
update();