Welcome! This guide will teach you the core patterns and best practices for building robust, reliable DeadZone plugins. We'll use real examples from production plugins to show you exactly how to structure your code.
A game tick happens approximately every 600 milliseconds (0.6 seconds). This is the fundamental timing unit in Old School RuneScape. Your OnGameTick() function is called once per tick.
OnGameTick should be a router, not a worker.
Your OnGameTick() function should:
function OnGameTick() {
// 1. Update metrics
totalTicksRunning++;
// 2. Update overlay
overlay.gameState.setValue(currentStage);
// 3. Block if executing actions
if (isExecuting) {
return;
}
// 4. Route to appropriate handler
switch (currentStage) {
case GameStage.BANKING:
HandleBanking();
break;
case GameStage.MINING:
HandleMining();
break;
case GameStage.WALKING:
HandleWalking();
break;
}
}
If your plugin executes actions every single tick with perfect timing (every 600ms exactly), it looks robotic. The delay system makes your actions appear human-like.
Bad:
function OnGameTick() {
if (!Game.info.bank.isOpen()) {
Game.interact.bank.openNearest(0, true); // Executes instantly every tick, no variation!
}
}
This will try to open the bank every 600ms with perfect timing, which is not natural behavior.
Good:
function HandleBanking() {
if (!Game.info.bank.isOpen()) {
isExecuting = true;
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
isExecuting = false;
}, Utility.getDelay()); // Random delay before executing
}
}
Utility.getDelay() returns a randomized delay based on DeadZone Action ProfilesisExecuting flag prevents the next tick from running while we waitfunction HandleMining() {
// Find a rock to mine
var rock = Game.info.gameObject.getNearest([ROCK_ID]);
if (rock != null) {
isExecuting = true; // Block next tick
Utility.invokeLater(function () {
Game.interact.gameObject.action(rock, MenuAction.GAME_OBJECT_FIRST_OPTION);
isExecuting = false; // Allow next tick to run
}, Utility.getDelay()); // Random delay
}
}
What happens:
isExecuting = trueisExecuting is still true, function returns earlyisExecuting = falseisExecuting is false, logic runs normallySometimes you need to perform multiple actions in sequence:
If you schedule both at the same time, they'll happen together. We need to accumulate delays.
function HandleCrafting() {
if (Game.info.inventory.hasItem(ITEM_ID_DARK_BLOCK, 1)) {
isExecuting = true;
// ACTION 1: Click the chisel
var delay = Utility.getDelay(); // e.g., 150ms
Utility.invokeLater(function () {
Game.interact.inventory.useItem(ITEM_ID_CHISEL, MenuAction.WIDGET_TARGET);
}, delay);
// ACTION 2: Click the essence (AFTER action 1)
delay += Utility.getDelay() + 300; // Now: 150 + 180 + 300 = 630ms
Utility.invokeLater(function () {
Game.interact.inventory.useItem(ITEM_ID_DARK_BLOCK, MenuAction.WIDGET_TARGET_ON_WIDGET);
isExecuting = false; // Clear flag ONLY after last action
}, delay);
return;
}
}
Tick 0ms:
- isExecuting = true
- delay = 150ms
- Schedule Action 1 at 150ms
- delay += 180 + 300 = 630ms
- Schedule Action 2 at 630ms
Time: 0ms -------- 150ms ---------- 630ms -------- 1200ms
↓ ↓ ↓
Start Click Chisel Click Essence Next Tick
+ clear flag (continues)
var delay = Utility.getDelay();delay += Utility.getDelay() + bufferisExecuting = false goes in the final callbackfunction HandleBanking() {
if (Game.info.bank.isOpen() && needItems) {
isExecuting = true;
var delay = Utility.getDelay();
// Withdraw pickaxe
Utility.invokeLater(function () {
Game.interact.bank.withdrawItem(ITEM_ID_PICKAXE, BankWithdrawType.WITHDRAW_ONE, false);
}, delay);
// Withdraw tinderbox after a delay
delay += Utility.getDelay() + 200;
Utility.invokeLater(function () {
Game.interact.bank.withdrawItem(ITEM_ID_TINDERBOX, BankWithdrawType.WITHDRAW_ONE, false);
}, delay);
// Close bank after another delay
delay += Utility.getDelay() + 400;
Utility.invokeLater(function () {
Game.interact.bank.close();
isExecuting = false; // Done with all actions
}, delay);
}
}
A state machine tracks what your plugin is currently doing. Instead of trying to handle everything at once, you break your plugin into distinct states.
Always use Object.freeze() to create immutable state definitions:
const GameStage = Object.freeze({
STARTUP: 'Startup',
MINING: 'Mining',
BANKING: 'Banking',
WALKING_TO_MINE: 'Walking to mine',
WALKING_TO_BANK: 'Walking to bank'
});
Why Object.freeze()?
┌─────────────┐
│ STARTUP │ (Validate equipment, location)
└──────┬──────┘
│
▼
┌─────────────┐
│ MINING │ (Fill inventory)
└──────┬──────┘
│
▼
┌─────────────────┐
│ WALKING_TO_BANK │ (Travel to bank)
└──────┬──────────┘
│
▼
┌─────────────┐
│ BANKING │ (Deposit ores)
└──────┬──────┘
│
▼
┌─────────────────┐
│ WALKING_TO_MINE │ (Return to mine)
└──────┬──────────┘
│
└─────► (loop back to MINING)
Step 1: Initialize state
var currentStage;
function OnStart() {
currentStage = GameStage.STARTUP;
}
Step 2: Route in OnGameTick
function OnGameTick() {
if (isExecuting) { return; }
switch (currentStage) {
case GameStage.STARTUP:
HandleStartup();
break;
case GameStage.MINING:
HandleMining();
break;
case GameStage.BANKING:
HandleBanking();
break;
}
}
Step 3: Create state handlers
function HandleMining() {
// Guards (early returns)
if (PlayerHelper.isMoving()) { return; }
if (!PlayerHelper.isPlayerIdle()) { return; }
// Check if inventory is full
if (Game.info.inventory.isFull()) {
currentStage = GameStage.WALKING_TO_BANK; // Transition to next state
return; // Don't mine, let next tick handle walking
}
// Mine the rock
var rock = Game.info.gameObject.getNearest([ROCK_ID]);
if (rock != null) {
isExecuting = true;
Utility.invokeLater(function () {
Game.interact.gameObject.action(rock, MenuAction.GAME_OBJECT_FIRST_OPTION);
isExecuting = false;
}, Utility.getDelay());
}
}
currentStage = GameStage.NEXT_STATE;// Good
if (inventoryFull) {
currentStage = GameStage.BANKING;
return; // Exit immediately
}
// Bad
if (inventoryFull) {
currentStage = GameStage.BANKING;
HandleBanking(); // Don't call directly!
}
Guard clauses are early return statements that prevent your code from running in invalid conditions. They make your code safer and more readable.
function HandleMining() {
// Guard 1: Don't act while moving
if (PlayerHelper.isMoving()) { return; }
// Guard 2: Don't act while already animating
if (!PlayerHelper.isPlayerIdle()) { return; }
// Guard 3: Don't act if rock is depleted
if (Game.getVarbitValue(VARBIT_ROCK_DEPLETED) != 0) { return; }
// All guards passed, safe to proceed
var rock = Game.info.gameObject.getNearest([ROCK_ID]);
// ... mining logic ...
}
Movement checks:
if (PlayerHelper.isMoving()) { return; }
if (PlayerHelper.isWebWalking()) { return; }
Animation checks:
if (!PlayerHelper.isPlayerIdle()) { return; } // Currently doing something
Inventory checks:
if (!Game.info.inventory.hasItem(ITEM_ID_PICKAXE, 1)) {
Game.sendGameMessage("No pickaxe found!", "My Plugin");
Utility.packages.shutdown();
return;
}
Location checks:
if (Client.getLocalPlayer().getWorldLocation().getRegionID() != EXPECTED_REGION) {
Game.sendGameMessage("Wrong location!", "My Plugin");
Utility.packages.shutdown();
return;
}
Always validate requirements before starting:
function HandleStartup() {
// Check for required items
if (!Game.info.inventory.hasItem(ITEM_ID_CHISEL, 1)) {
Game.sendGameMessage("Could not find a chisel!", packageName);
Utility.packages.shutdown();
return;
}
// Check for equipment
if (!Game.info.inventory.hasItems(ITEM_ID_PICKAXE_IDS, 1) &&
!Game.info.equipment.hasItems(ITEM_ID_PICKAXE_IDS)) {
Game.sendGameMessage("Could not find a pickaxe!", packageName);
Utility.packages.shutdown();
return;
}
// Check location
if (Client.getLocalPlayer().getWorldLocation().getRegionID() != MINING_REGION_ID) {
Game.sendGameMessage("Please start at the mine!", packageName);
Utility.packages.shutdown();
return;
}
// All checks passed
Game.sendGameMessage("Starting...", packageName);
currentStage = GameStage.MINING;
}
// BAD - This runs every 600ms exactly
function OnGameTick() {
if (!Game.info.bank.isOpen()) {
Game.interact.bank.openNearest(0, true);
}
}
// GOOD - Random delays
function HandleBanking() {
if (!Game.info.bank.isOpen()) {
isExecuting = true;
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
isExecuting = false;
}, Utility.getDelay());
}
}
// BAD - Always exactly 200ms
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
}, 200);
// GOOD - Random delays from user profile
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
}, Utility.getDelay());
// BAD - Can execute multiple actions at once
function HandleBanking() {
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
}, Utility.getDelay());
}
// GOOD - Blocks next tick
function HandleBanking() {
isExecuting = true;
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
isExecuting = false;
}, Utility.getDelay());
}
// BAD - Both actions happen at same time
function HandleCrafting() {
var delay = Utility.getDelay(); // e.g., 150ms
Utility.invokeLater(function () {
// Action 1
}, delay); // Happens at 150ms
Utility.invokeLater(function () {
// Action 2
}, delay); // Also happens at 150ms!
}
// GOOD - Actions happen in sequence
function HandleCrafting() {
var delay = Utility.getDelay();
Utility.invokeLater(function () {
// Action 1
}, delay); // Happens at 150ms
delay += Utility.getDelay() + 300; // Now 630ms
Utility.invokeLater(function () {
// Action 2
}, delay); // Happens at 630ms
}
// BAD - Bypasses tick system
function HandleMining() {
if (Game.info.inventory.isFull()) {
currentStage = GameStage.BANKING;
HandleBanking(); // Don't call directly!
}
}
// GOOD - Let next tick handle it
function HandleMining() {
if (Game.info.inventory.isFull()) {
currentStage = GameStage.BANKING;
return; // Next tick will call HandleBanking()
}
}
// BAD - Logic mixed into OnGameTick
function OnGameTick() {
totalTicksRunning++;
if (currentStage == GameStage.MINING) {
if (!PlayerHelper.isMoving() && PlayerHelper.isPlayerIdle()) {
var rock = Game.info.gameObject.getNearest([ROCK_ID]);
if (rock != null) {
// ... mining logic here ...
}
}
}
// ... more logic ...
}
// GOOD - OnGameTick routes to handlers
function OnGameTick() {
totalTicksRunning++;
if (isExecuting) { return; }
switch (currentStage) {
case GameStage.MINING:
HandleMining(); // Logic in separate function
break;
}
}
Questions?
The Discord is a great place where staff and other programmers can help you out, don't be afraid to reach out and ask for help.