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 normallyWhen you execute an action (like clicking a bank or attacking an NPC), the game needs time to process it and update the client. If your plugin tries to execute the same action again before the game has caught up, you'll end up spamming actions unnecessarily.
To prevent this, we need a way to "pause" the plugin's logic for a tick or two, giving the game time to process the action and update the game state.
This is the most common approach. You use a boolean flag called isExecuting to track whether an action is currently being processed.
var isExecuting = false;
function OnGameTick() {
// Block if we're waiting for an action to complete
if (isExecuting) {
return; // Skip this tick
}
// Route to handler
if (currentStage == GameStage.BANKING) {
HandleBanking();
}
}
function HandleBanking() {
if (!Game.info.bank.isOpen()) {
isExecuting = true; // Set flag before scheduling
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
isExecuting = false; // Clear flag when done
}, Utility.getDelay());
}
}
How it works:
isExecuting = trueOnGameTick() from running your handlers on the next tickisExecuting = falsePros:
Cons:
This approach tracks the last tick when an action was executed, and only allows new actions after a certain number of ticks have passed.
var lastActionTick = 0;
var TICKS_TO_WAIT = 2; // Wait 2 ticks before next action
function OnGameTick() {
var currentTick = Client.getGameTick();
// Block if not enough ticks have passed
if (currentTick - lastActionTick < TICKS_TO_WAIT) {
return; // Skip this tick
}
// Route to handler
if (currentStage == GameStage.BANKING) {
HandleBanking();
}
}
function HandleBanking() {
if (!Game.info.bank.isOpen()) {
Utility.invokeLater(function () {
Game.interact.bank.openNearest(0, true);
}, Utility.getDelay());
lastActionTick = Client.getGameTick(); // Record when we acted
}
}
How it works:
Pros:
TICKS_TO_WAITCons:
lastActionTick after every actionBoth methods are valid! Choose the one that feels more natural for your plugin:
You can even combine both methods: use the flag for most actions, and use tick tracking for specific scenarios where you need precise timing.
Sometimes 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)
// Add another delay for the second action
delay += Utility.getDelay(); // Now: 150 + 180 = 330ms
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 (first Utility.getDelay())
- Schedule Action 1 at 150ms
- delay += 180ms (second Utility.getDelay())
- Schedule Action 2 at 330ms
Time: 0ms -------- 150ms ---------- 330ms -------- 600ms
↓ ↓ ↓
Start Click Chisel Click Essence Next Tick
+ clear flag (continues)
var delay = Utility.getDelay();delay += Utility.getDelay();Utility.getDelay() provide variationisExecuting = 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();
Utility.invokeLater(function () {
Game.interact.bank.withdrawItem(ITEM_ID_TINDERBOX, BankWithdrawType.WITHDRAW_ONE, false);
}, delay);
// Close bank after another delay
delay += Utility.getDelay();
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;
}
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.