원클릭으로
openrct2
Develop plugins for OpenRCT2 - JavaScript/TypeScript scripting, game actions, UI windows, hooks, multiplayer sync
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Develop plugins for OpenRCT2 - JavaScript/TypeScript scripting, game actions, UI windows, hooks, multiplayer sync
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Develop C++ plugins for Kate text editor using KTextEditor Framework, CMake with ECM, Qt threading, and KDE plugin architecture
Develop plugins, tools, and extensions for OpenCode AI coding agent with MCP, LSP integration, custom tools, and SDK usage
Django security - CSRF protection, authentication, sessions, login/logout, password handling, middleware, protected views
Python testing framework with powerful fixtures, parametrization, extensive plugin ecosystem, and support for async, Django, Flask testing
Python SQL toolkit and ORM with expressive query API, relationship mapping, async support, and Alembic migrations
Common Rust development pitfalls: frequent compiler errors, struct constructor patterns, test organization, and coverage enforcement for reliable codebases.
| name | openrct2 |
| description | Develop plugins for OpenRCT2 - JavaScript/TypeScript scripting, game actions, UI windows, hooks, multiplayer sync |
| metadata | {"author":"mte90","version":"1.0.0","tags":["openrct2","plugin","javascript","typescript","game-modding","rollercoaster-tycoon","scripting"]} |
Comprehensive guide for developing plugins (scripts) for OpenRCT2, the open-source re-implementation of RollerCoaster Tycoon 2.
OpenRCT2 allows custom scripts (plugins) written in JavaScript/TypeScript to extend the game with additional behavior - from extra windows to entire multiplayer game modes.
Place .js files in the plugin directory:
C:\Users\YourName\Documents\OpenRCT2\plugin\/Users/YourName/Library/Application Support/OpenRCT2/plugin/$XDG_CONFIG_HOME/OpenRCT2/plugin/ or $HOME/.config/OpenRCT2/plugin/Access via game: Red toolbox button → Open custom content folder
function main() {
console.log("Your plugin has started!");
// Your plugin code here
}
registerPlugin({
name: 'Your Plugin',
version: '1.0',
authors: ['Your Name'],
type: 'remote',
licence: 'MIT',
targetApiVersion: 34,
minApiVersion: 10,
main: main
});
// Install TypeScript and types
// npm install typescript --save-dev
// Copy openrct2.d.ts to your project
/// <reference path="openrct2.d.ts" />
function main() {
console.log("TypeScript plugin loaded!");
}
registerPlugin({
name: 'My TypeScript Plugin',
version: '1.0',
authors: ['Developer'],
type: 'local',
licence: 'MIT',
targetApiVersion: 34,
main: main
});
{
"compilerOptions": {
"target": "ES5",
"module": "none",
"outFile": "./dist/plugin.js",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Load on any client with plugin installed. Cannot alter game state directly.
registerPlugin({
name: 'Local Info Plugin',
version: '1.0',
type: 'local', // Available to all players in multiplayer
// ...
main: function() {
// Can only use game actions, not direct mutations
// Good for: info windows, tools, dashboards
}
});
Load only on server, distributed to clients. Can mutate game state in execute context.
registerPlugin({
name: 'Remote Game Plugin',
version: '1.0',
type: 'remote', // Server-side, synced to clients
// ...
main: function() {
// Can mutate game state in custom game action execute()
}
});
Stay loaded across park changes and in title screen.
registerPlugin({
name: 'Global Plugin',
version: '1.0',
type: 'intransient', // Never unloaded
// ...
main: function() {
// Active in title screen and across parks
// Use context.sharedStorage for persistence
}
});
Game actions are the recommended way to mutate game state, ensuring multiplayer synchronization.
// Place scenery using game action
var action = {
type: 'smallsceneryplace',
args: {
object: 0, // Scenery object ID
x: 32 * 10, // X coordinate in map units
y: 32 * 10, // Y coordinate in map units
z: 0, // Z height
direction: 0, // Rotation (0-3)
quadrant: 0, // Quadrant for quarter tile scenery
primaryColour: 0, // Primary color
secondaryColour: 0 // Secondary color
}
};
context.executeAction(action, function(result) {
if (result.error) {
console.log("Failed to place scenery: " + result.error);
} else {
console.log("Scenery placed successfully");
}
});
// Set park cash
context.executeAction({
type: 'parksetcash',
args: { cash: 100000 }
}, callback);
// Set guest count
context.executeAction({
type: 'parksetguestgenerationrate',
args: { generationRate: 100 }
}, callback);
// Change land height
context.executeAction({
type: 'landsetheight',
args: {
x: 32 * 10,
y: 32 * 10,
height: 10
}
}, callback);
// Build ride
context.executeAction({
type: 'trackplace',
args: {
ride: 0,
trackType: 1,
x: 32 * 10,
y: 32 * 10,
z: 0,
direction: 0
}
}, callback);
// Register custom action
context.registerAction({
id: 'myplugin.award_cash',
query: function(args) {
// Validation - return error object if invalid
if (args.amount < 0) {
return { error: 'Amount must be positive' };
}
if (args.amount > 100000) {
return { error: 'Amount too large' };
}
return {}; // Success
},
execute: function(args) {
// Actual game state mutation - only runs on server
park.cash += args.amount;
return {}; // Success
}
});
// Use custom action
context.executeAction({
type: 'myplugin.award_cash',
args: { amount: 5000 }
}, function(result) {
console.log(result.error || "Cash awarded!");
});
context.registerAction({
id: 'myplugin.admin_action',
query: function(args) {
// Check player permissions
if (network.mode !== 'none') {
var player = network.getPlayer(args.playerId);
if (!player || !player.hasPermission('modify_park')) {
return { error: 'No permission' };
}
}
return {};
},
execute: function(args) {
// Perform action
}
});
// Always check before using UI APIs (for headless servers)
if (typeof ui !== 'undefined') {
// UI is available
ui.registerMenuItem('My Window', openWindow);
}
function main() {
if (typeof ui === 'undefined') return;
// Add menu item
ui.registerMenuItem('My Plugin', function() {
openMainWindow();
});
// Add to specific menu tab
ui.registerMenuItem('Ride Stats', function() {
openRideStatsWindow();
}, 'ride'); // 'map', 'park', 'ride', 'guest', etc.
}
function openMainWindow() {
var window = ui.openWindow({
classification: 'myplugin.main',
title: 'My Plugin Window',
width: 300,
height: 200,
widgets: [
// Label
{
type: 'label',
x: 10,
y: 10,
width: 280,
height: 20,
text: 'Hello, OpenRCT2!'
},
// Button
{
type: 'button',
x: 10,
y: 40,
width: 120,
height: 30,
text: 'Click Me',
onClick: function() {
console.log('Button clicked!');
}
},
// Checkbox
{
type: 'checkbox',
x: 10,
y: 80,
width: 200,
height: 20,
text: 'Enable feature',
isChecked: false,
onChange: function(checked) {
console.log('Checkbox: ' + checked);
}
},
// Dropdown
{
type: 'dropdown',
x: 10,
y: 110,
width: 200,
height: 20,
items: ['Option 1', 'Option 2', 'Option 3'],
selectedIndex: 0,
onChange: function(index) {
console.log('Selected: ' + index);
}
},
// Slider
{
type: 'slider',
x: 10,
y: 140,
width: 200,
height: 20,
minValue: 0,
maxValue: 100,
value: 50,
onChange: function(value) {
console.log('Slider: ' + value);
}
},
// Spinner
{
type: 'spinner',
x: 10,
y: 170,
width: 100,
height: 20,
text: '10',
onDecrement: function() {
// Handle decrement
},
onIncrement: function() {
// Handle increment
}
}
]
});
return window;
}
{
type: 'listview',
x: 10,
y: 10,
width: 280,
height: 150,
scrollbars: 'vertical',
isStriped: true,
showColumnHeaders: true,
columns: [
{ header: 'Name', width: 150 },
{ header: 'Value', width: 80 },
{ header: 'Status', width: 50 }
],
items: [
['Ride 1', '$5000', 'OK'],
['Ride 2', '$3000', 'OK'],
['Ride 3', '$2000', 'Low']
],
onHighlight: function(item, column) {
console.log('Highlighted: ' + item + ', ' + column);
},
onClick: function(item, column) {
console.log('Clicked: ' + item + ', ' + column);
}
}
{
type: 'groupbox',
x: 10,
y: 10,
width: 280,
height: 100,
text: 'Settings',
widgets: [
{
type: 'checkbox',
x: 10,
y: 20,
width: 260,
height: 20,
text: 'Enable notifications'
},
{
type: 'checkbox',
x: 10,
y: 45,
width: 260,
height: 20,
text: 'Auto-save'
}
]
}
function openTabWindow() {
var window = ui.openWindow({
classification: 'myplugin.tabs',
title: 'Tabbed Window',
width: 400,
height: 300,
tabs: [
{
image: 5221, // Icon ID
widgets: [
{
type: 'label',
x: 10, y: 10,
width: 380, height: 20,
text: 'Tab 1 Content'
}
]
},
{
image: 5222,
widgets: [
{
type: 'label',
x: 10, y: 10,
width: 380, height: 20,
text: 'Tab 2 Content'
}
]
}
]
});
}
var window = ui.openWindow({ /* ... */ });
window.onClose = function() {
console.log('Window closed');
// Cleanup
};
// Every game tick (40ms)
context.subscribe('interval.tick', function() {
// Runs 25 times per second
});
// Every game day
context.subscribe('interval.day', function() {
console.log('New day! Cash: ' + park.cash);
// Award daily bonus
park.cash += 1000;
});
// Every in-game hour
context.subscribe('interval.hour', function() {
// Hourly updates
});
// Ride created
context.subscribe('ride.ratings.calculate', function(e) {
var ride = map.getRide(e.rideId);
console.log('Ride ratings calculated: ' + ride.name);
});
// Ride crashed
context.subscribe('ride.crashed', function(e) {
console.log('Ride ' + e.rideId + ' crashed!');
});
// Guest entered park
context.subscribe('guest.entered_park', function(e) {
var guest = map.getEntity(e.entityId);
console.log('Guest ' + guest.id + ' entered park');
});
// Guest left park
context.subscribe('guest.left_park', function(e) {
console.log('Guest left: ' + e.entityId);
});
// Guest bought item
context.subscribe('guest.bought_item', function(e) {
console.log('Guest bought item: ' + e.item);
});
// Chat message received
context.subscribe('network.chat', function(e) {
console.log(e.playerName + ': ' + e.message);
// Anti-spam example
if (e.message.length > 200) {
network.kickPlayer(e.playerId);
}
});
// Player joined
context.subscribe('network.join', function(e) {
console.log('Player joined: ' + e.playerName);
// Welcome message
network.sendMessage('Welcome to the server, ' + e.playerName + '!');
});
// Player left
context.subscribe('network.leave', function(e) {
console.log('Player left: ' + e.playerName);
});
// Before action executes
context.subscribe('action.query', function(e) {
if (e.action === 'ridesetstatus') {
console.log('Ride status changing...');
}
});
// After action executes
context.subscribe('action.execute', function(e) {
if (e.action === 'ridesetstatus') {
console.log('Ride status changed');
}
});
// Park stats
console.log('Park name: ' + park.name);
console.log('Cash: $' + park.cash);
console.log('Rating: ' + park.rating);
console.log('Guests: ' + park.guests);
console.log('Value: $' + park.value);
console.log('Company value: $' + park.companyValue);
// Park flags
if (park.getFlag('noMoney')) {
console.log('Park has no money');
}
// Modify park (remote plugin only)
park.cash = 100000;
park.name = "My Awesome Park";
// Get map size
var mapSize = map.size;
console.log('Map size: ' + mapSize.x + 'x' + mapSize.y);
// Iterate all tiles
for (var x = 0; x < map.size.x; x++) {
for (var y = 0; y < map.size.y; y++) {
var tile = map.getTile(x, y);
// Process tile
}
}
// Get tile at coordinates
var tile = map.getTile(10, 10);
// Tile elements
for (var i = 0; i < tile.numElements; i++) {
var element = tile.getElement(i);
if (element.type === 'surface') {
console.log('Surface at ' + element.baseHeight);
} else if (element.type === 'track') {
console.log('Track element');
} else if (element.type === 'small_scenery') {
console.log('Small scenery');
}
}
// Get all rides
var rides = map.rides;
for (var i = 0; i < rides.length; i++) {
var ride = rides[i];
console.log(ride.name + ' - Excitement: ' + ride.excitement);
}
// Get specific ride
var ride = map.getRide(0);
// Ride properties
console.log('Type: ' + ride.type);
console.log('Status: ' + ride.status);
console.log('Excitement: ' + ride.excitement);
console.log('Intensity: ' + ride.intensity);
console.log('Nausea: ' + ride.nausea);
console.log('Price: $' + ride.price);
console.log('Customers: ' + ride.customers);
// Modify ride (remote only)
ride.price = 500; // $5.00
ride.name = "Super Coaster";
// Get all entities
var entities = map.entities;
// Iterate guests
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (entity.type === 'guest') {
console.log('Guest ' + entity.id);
console.log(' Cash: $' + entity.cash);
console.log(' Happiness: ' + entity.happiness);
console.log(' Energy: ' + entity.energy);
console.log(' Hunger: ' + entity.hunger);
console.log(' Thirst: ' + entity.thirst);
} else if (entity.type === 'staff') {
console.log('Staff ' + entity.id);
console.log(' Type: ' + entity.staffType);
}
}
// Get specific entity
var guest = map.getEntity(entityId);
// Modify guest (remote only)
guest.happiness = 200;
guest.energy = 150;
guest.cash = 500;
// Check network mode
if (network.mode === 'server') {
console.log('Running as server');
} else if (network.mode === 'client') {
console.log('Running as client');
} else {
console.log('Single player');
}
// Get all players
var players = network.players;
for (var i = 0; i < players.length; i++) {
var player = players[i];
console.log(player.name + ' (ID: ' + player.id + ')');
}
// Get specific player
var player = network.getPlayer(playerId);
// Player properties
console.log('Name: ' + player.name);
console.log('Group: ' + player.group);
console.log('Ping: ' + player.ping);
// Kick player
network.kickPlayer(playerId);
// Send message
network.sendMessage('Hello everyone!');
network.sendMessage('Private message', [playerId]); // To specific player
// Create server (localhost only)
var server = network.createListener();
server.on('connection', function(conn) {
console.log('Client connected');
conn.on('data', function(data) {
console.log('Received: ' + data);
conn.write('Echo: ' + data);
});
conn.on('close', function() {
console.log('Client disconnected');
});
});
server.listen(8080, function() {
console.log('Server listening on port 8080');
});
// Create client
var client = network.createSocket();
client.on('connect', function() {
console.log('Connected to server');
client.write('Hello from OpenRCT2');
});
client.on('data', function(data) {
console.log('Server says: ' + data);
});
client.connect(8080, 'localhost');
// Persistent across all parks (plugin.store.json)
var myData = context.sharedStorage.get('myplugin.data', { defaultValue: 0 });
context.sharedStorage.set('myplugin.data', myData + 1);
// Namespaced keys recommended
context.sharedStorage.set('MyPlugin.Settings.Enabled', true);
context.sharedStorage.set('MyPlugin.Settings.Volume', 0.8);
// Saved with the park file
var parkData = context.getParkStorage('myplugin');
var counter = parkData.get('counter', 0);
parkData.set('counter', counter + 1);
Edit config.ini:
[plugin]
enable_hot_reloading = true
// Plugin with auto-reload support
var window = null;
function main() {
console.log('Plugin loaded/reloaded!');
// Close old window on reload
if (window) {
window.close();
}
// Create new window
openWindow();
}
function openWindow() {
window = ui.openWindow({
classification: 'myplugin.dev',
title: 'Dev Window',
width: 200,
height: 100,
widgets: [
{
type: 'label',
x: 10, y: 10,
width: 180, height: 80,
text: 'Edit and save JS to reload!'
}
]
});
}
// Good: Check before UI operations
function main() {
if (typeof ui !== 'undefined') {
ui.registerMenuItem('My Window', openWindow);
}
// Non-UI code runs everywhere
context.subscribe('interval.day', onDay);
}
// Good: Use game action
context.executeAction({
type: 'parksetcash',
args: { cash: 100000 }
}, callback);
// Bad: Direct mutation in local plugin
// park.cash = 100000; // Will fail in multiplayer!
// Good: Use namespace prefix
context.sharedStorage.set('MyPlugin.Key', value);
// Bad: Generic key may conflict
context.sharedStorage.set('data', value);
context.executeAction(action, function(result) {
if (result.error) {
console.log('Action failed: ' + result.error);
return;
}
// Success handling
});
var intervals = [];
function main() {
intervals.push(context.setInterval(update, 1000));
}
// For remote/intransient plugins
context.subscribe('map.changed', function() {
// Clear intervals when map changes
intervals.forEach(clearInterval);
intervals = [];
});
OpenRCT2 uses Duktape (ES5). ES6+ features require transpilation:
// Arrow functions
var func = () => {}; // NO
// Classes
class MyClass {} // NO
// let/const
let x = 1; // NO
// Template literals
`Hello ${name}` // NO
// Spread operator
[...arr] // NO
// Destructuring
var { x } = obj; // NO
// find/includes
arr.find(x => x > 0); // NO
arr.includes(5); // NO
// Function expressions
var func = function() {}; // YES
// Constructor functions
function MyClass() {} // YES
// var
var x = 1; // YES
// String concatenation
'Hello ' + name // YES
// Array methods
arr.filter(function(x) { return x > 0; })[0]; // YES
arr.indexOf(5) !== -1; // YES
// Basic logging
console.log('Debug message');
console.log('Value: ' + value);
// Object inspection
console.log(JSON.stringify(obj));
Run openrct2.com (Windows) or terminal version to access interactive console.
// In console, test expressions
> park.cash
> map.rides.length
> context.sharedStorage.get('myplugin.data')
.jsregisterPlugin({
name: 'My Plugin',
version: '1.2.3', // Semantic versioning
minApiVersion: 34, // Minimum OpenRCT2 API version
targetApiVersion: 34, // Target API for behavior
// ...
});