DeadZone Community Packages
    Preparing search index...

    Best Practices

    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.

    1. Understanding the GameTick
    2. The Delay System
    3. Chaining Multiple Actions
    4. State Machines
    5. Guard Clauses
    6. Common Patterns
    7. What NOT to Do

    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:

    • Check conditions
    • Route to the appropriate handler
    • NOT contain business logic directly
    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;
    }
    }
    • Clean separation: Each task has its own function
    • Easy to debug: You can see exactly which handler is running
    • Maintainable: Adding new states is simple
    • Readable: Anyone can understand the flow at a glance

    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
    }
    }
    1. Utility.getDelay() returns a randomized delay based on DeadZone Action Profiles
    2. The action is scheduled to execute after that delay
    3. The timing varies each time, appearing more human-like
    4. The isExecuting flag prevents the next tick from running while we wait
    function 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:

    • Tick 1: Find rock, schedule action for +150ms, set isExecuting = true
    • Tick 2 (at 600ms): isExecuting is still true, function returns early
    • At 750ms: Action executes, isExecuting = false
    • Tick 3 (at 1200ms): isExecuting is false, logic runs normally

    Sometimes you need to perform multiple actions in sequence:

    1. Click the chisel
    2. Wait a moment
    3. Click the item to craft

    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)
    1. Start with initial delay: var delay = Utility.getDelay();
    2. Accumulate for each action: delay += Utility.getDelay() + buffer
    3. Add human-like buffers: +300ms between actions feels natural
    4. Clear flag in LAST action only: isExecuting = false goes in the final callback
    function 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()?

    • Prevents accidental modification
    • Makes states act like enums
    • Catches typos at runtime
    ┌─────────────┐
    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());
    }
    }
    1. Transition explicitly: currentStage = GameStage.NEXT_STATE;
    2. Return after transitioning: Don't execute current state's logic
    3. Let next tick handle the new state: Clean separation between states
    // 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.