Appearance
Plugin Examples
Real-world plugin examples to learn from.
FPS Counter
A simple in-game FPS display.
plugin.js:
javascript
module.exports = {
name: 'FPS Counter',
version: '1.0.0',
inject: ['style.css', 'template.html', 'script.js'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show FPS', default: true },
{ id: 'position', type: 'select', options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], default: 'top-left' },
{ id: 'color', type: 'color', label: 'Text Color', default: '#4ade80' }
]
};template.html:
html
{{#if enabled}}
<div class="fps-counter {{position}}" id="fps" style="color: {{color}}">-- FPS</div>
{{/if}}style.css:
css
.fps-counter { position: fixed; padding: 4px 8px; background: rgba(0,0,0,0.7); font: 14px monospace; 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; }script.js:
javascript
const { enabled } = GEMSHELL_SETTINGS;
if (!enabled) return;
const el = document.getElementById('fps');
let frames = 0, last = performance.now();
(function loop() {
frames++;
const now = performance.now();
if (now - last >= 1000) {
el.textContent = frames + ' FPS';
frames = 0;
last = now;
}
requestAnimationFrame(loop);
})();Discord Webhook
Send build notifications to Discord.
plugin.js:
javascript
module.exports = {
name: 'Discord Notifier',
version: '1.0.0',
settings: [
{ id: 'webhookUrl', type: 'text', label: 'Webhook URL', placeholder: 'https://discord.com/api/webhooks/...' },
{ id: 'onSuccess', type: 'checkbox', label: 'Notify on success', default: true },
{ id: 'onError', type: 'checkbox', label: 'Notify on error', default: true }
],
async onPostBuild(context, settings) {
if (!settings.onSuccess || !settings.webhookUrl) return;
await gemshell.http.post(settings.webhookUrl, {
embeds: [{
title: 'Build Complete',
description: `**${context.config.title}** v${context.config.version}`,
color: 0x4ade80,
fields: [
{ name: 'Platform', value: `${context.platform}-${context.arch}`, inline: true }
],
timestamp: new Date().toISOString()
}]
});
},
async onBuildError(context, settings) {
if (!settings.onError || !settings.webhookUrl) return;
await gemshell.http.post(settings.webhookUrl, {
embeds: [{
title: 'Build Failed',
description: context.error,
color: 0xef4444,
timestamp: new Date().toISOString()
}]
});
}
};Watermark
Add a watermark to demo builds.
plugin.js:
javascript
module.exports = {
name: 'Watermark',
version: '1.0.0',
inject: ['style.css', 'template.html'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show Watermark', default: true },
{ id: 'text', type: 'text', label: 'Text', default: 'DEMO' },
{ id: 'opacity', type: 'slider', label: 'Opacity', min: 10, max: 100, default: 50 }
]
};template.html:
html
{{#if enabled}}
<div class="watermark" style="opacity: {{opacity}}%">{{text}}</div>
{{/if}}style.css:
css
.watermark {
position: fixed;
bottom: 20px;
right: 20px;
font: bold 24px sans-serif;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
pointer-events: none;
z-index: 99999;
}Build Counter
Track build numbers.
plugin.js:
javascript
module.exports = {
name: 'Build Counter',
version: '1.0.0',
async onPreBuild(context, settings) {
const count = gemshell.storage.get('buildCount', 0) + 1;
gemshell.storage.set('buildCount', count);
gemshell.log(`Build #${count}`);
},
async onModifyAssets(context, settings) {
const count = gemshell.storage.get('buildCount', 1);
// Add build number to game
gemshell.transform.injectIntoHtml(
`<script>window.BUILD_NUMBER = ${count};</script>`,
'head'
);
}
};JSON Minifier
Reduce JSON file sizes.
plugin.js:
javascript
module.exports = {
name: 'JSON Minifier',
version: '1.0.0',
async onModifyAssets(context, settings) {
const files = gemshell.glob('**/*.json');
let saved = 0;
for (const file of files) {
try {
const original = gemshell.fs.read(file);
const data = JSON.parse(original);
const minified = JSON.stringify(data);
if (minified.length < original.length) {
gemshell.fs.write(file, minified);
saved += original.length - minified.length;
}
} catch (e) {
gemshell.warn(`Could not minify ${file}`);
}
}
gemshell.log(`Saved ${(saved / 1024).toFixed(1)} KB`);
}
};Save Encryption
Encrypt save files.
plugin.js:
javascript
module.exports = {
name: 'Save Encryptor',
version: '1.0.0',
settings: [
{ id: 'key', type: 'text', label: 'Encryption Key', placeholder: 'Enter a secret key' },
{ id: 'pattern', type: 'text', label: 'File Pattern', default: 'saves/*.json' }
],
async onModifyAssets(context, settings) {
if (!settings.key) {
gemshell.warn('No encryption key set');
return;
}
const files = gemshell.glob(settings.pattern);
for (const file of files) {
const content = gemshell.fs.read(file);
const encrypted = gemshell.native.encrypt(settings.key, content);
if (encrypted) {
gemshell.fs.write(file + '.enc', encrypted);
gemshell.fs.remove(file);
gemshell.log(`Encrypted: ${file}`);
}
}
}
};Chrome Flags
Set custom Chrome/Electron command line flags for performance tuning.
plugin.js:
javascript
module.exports = {
name: 'Chrome Flags',
version: '1.0.0',
description: 'Add custom Chrome command line flags to your game',
settings: [
{
id: 'flags',
type: 'textarea',
label: 'Chrome Flags',
description: 'Space or newline separated (e.g. --disable-frame-rate-limit)',
default: '',
rows: 4
}
],
onGenerateRuntime(context, settings) {
const flagsStr = settings.flags || '';
if (!flagsStr.trim()) return '';
// Parse flags: split by whitespace or newlines
const flags = flagsStr
.split(/[\s\n]+/)
.map(f => f.trim())
.filter(f => f.startsWith('--'));
if (flags.length === 0) return '';
// Generate code for each flag
return flags.map(flag => {
if (flag.includes('=')) {
const [name, value] = flag.split('=');
return `_a.commandLine.appendSwitch('${name.replace(/^--/, '')}','${value}');`;
}
return `_a.commandLine.appendSwitch('${flag.replace(/^--/, '')}');`;
}).join('\n');
}
};Common flags:
| Flag | Effect |
|---|---|
--disable-frame-rate-limit | Unlock FPS (removes 60fps cap) |
--disable-gpu-vsync | Disable VSync |
--enable-gpu-rasterization | GPU-accelerated rasterization |
--ignore-gpu-blocklist | Use GPU on all hardware |
--disable-software-rasterizer | Force hardware rendering |
--use-angle=d3d11 | Use DirectX 11 on Windows |
Version Banner
Show version info in-game.
plugin.js:
javascript
module.exports = {
name: 'Version Banner',
version: '1.0.0',
inject: ['style.css', 'template.html'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show Version', default: true }
],
async onModifyAssets(context, settings) {
if (!settings.enabled) return;
// Store version for template
gemshell.transform.injectIntoHtml(
`<script>window.GAME_VERSION = "${context.config.version}";</script>`,
'head'
);
}
};template.html:
html
{{#if enabled}}
<div class="version-banner" id="version-banner"></div>
{{/if}}style.css:
css
.version-banner {
position: fixed;
bottom: 5px;
left: 5px;
font: 10px monospace;
color: rgba(255,255,255,0.5);
pointer-events: none;
z-index: 99999;
}script.js:
javascript
const { enabled } = GEMSHELL_SETTINGS;
if (!enabled) return;
const el = document.getElementById('version-banner');
if (el && window.GAME_VERSION) {
el.textContent = 'v' + window.GAME_VERSION;
}