DeadZone Community Packages
    Preparing search index...

    Plugin Development 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. Preventing Duplicate Actions
    4. Chaining Multiple Actions
    5. State Machines
    6. Guard Clauses
    7. Common Patterns

    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

    When 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:

    • Before scheduling an action, set isExecuting = true
    • This blocks OnGameTick() from running your handlers on the next tick
    • When the action completes, set isExecuting = false
    • The next tick can now proceed normally

    Pros:

    • Simple and easy to understand
    • Works well for most use cases
    • Easy to debug

    Cons:

    • Requires manually managing the flag in every action
    • If you forget to clear the flag, your plugin will hang

    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:

    • Track the game tick when you last executed an action
    • Before running handlers, check if enough ticks have passed
    • If not enough ticks have passed, skip the current tick

    Pros:

    • No need to manually clear a flag
    • Can easily adjust wait time by changing TICKS_TO_WAIT
    • Less prone to "hanging" bugs

    Cons:

    • Slightly more complex to understand
    • Need to remember to update lastActionTick after every action

    Both methods are valid! Choose the one that feels more natural for your plugin:

    • Use Method 1 (isExecuting flag) if you want simple on/off blocking and don't mind managing the flag
    • Use Method 2 (game tick tracking) if you prefer automatic timing and want more control over wait duration

    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:

    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)
    // 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)
    1. Start with initial delay: var delay = Utility.getDelay();
    2. Accumulate for each action: delay += Utility.getDelay();
    3. Always use randomized delays: Never hardcode fixed values - let Utility.getDelay() provide variation
    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();
    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()?

    • 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;
    }

    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.