Skip to content

Executions

Executions are the core mechanism for modifying game state.

Overview

An Execution represents a game state change that will be applied:

import { Execution, Game } from '@lands.io/mod-sdk';

class DonateExecution extends Execution {
  constructor(
    private donorId: string,
    private recipientId: string,
    private amount: number
  ) {
    super();
  }

  execute(game: Game): void {
    const donor = game.player(this.donorId);
    const recipient = game.player(this.recipientId);

    if (donor && recipient) {
      donor.removeTroops(this.amount);
      recipient.addTroops(this.amount);
    }
  }
}

Execution Lifecycle

  1. Creation - Action or intent handler creates the execution
  2. Validation - Engine validates the execution is legal
  3. Execution - execute() is called to modify game state
  4. Sync - Changes are broadcast to all clients

Creating Executions

From Actions

class MyAction extends BaseAction {
  createExecution(context: ActionContext, target: ActionTarget): Execution {
    return new MyExecution(context.player.id(), target);
  }
}

From Intent Handlers

registerIntentHandlers(executor: Executor): void {
  executor.registerModIntentHandler(executor.modId, 'donate', (intent) => {
    const { recipientId, amount } = intent.payload;
    return new DonateExecution(intent.playerID, recipientId, amount);
  });
}

NoOpExecution

Return this when you want to acknowledge an intent but do nothing:

import { NoOpExecution } from '@lands.io/mod-sdk';

executor.registerModIntentHandler(executor.modId, 'donate', (intent) => {
  if (!isValidDonation(intent)) {
    return new NoOpExecution(); // Valid request, but we choose to do nothing
  }
  return new DonateExecution(/* ... */);
});

Example: Full Donate Execution

import { Execution, Game, ModState } from '@lands.io/mod-sdk';

export class DonateExecution extends Execution {
  private static readonly COOLDOWN_TICKS = 100;

  constructor(
    private donorId: string,
    private recipientId: string,
    private percentage: number,
    private modState: ModState
  ) {
    super();
  }

  execute(game: Game): void {
    const donor = game.player(this.donorId);
    const recipient = game.player(this.recipientId);

    if (!donor || !recipient) return;
    if (!donor.isAlive() || !recipient.isAlive()) return;

    // Check cooldown
    const cooldowns = this.modState.get('cooldowns') || {};
    const lastDonation = cooldowns[this.donorId] || 0;
    if (game.tick() - lastDonation < DonateExecution.COOLDOWN_TICKS) {
      return;
    }

    // Calculate donation amount
    const donationAmount = Math.floor(donor.troops() * (this.percentage / 100));
    if (donationAmount <= 0) return;

    // Execute the transfer
    donor.removeTroops(donationAmount);
    recipient.addTroops(donationAmount);

    // Update cooldown
    cooldowns[this.donorId] = game.tick();
    this.modState.set('cooldowns', cooldowns);
  }
}

Best Practices

Validation

Always validate inputs in execute(). Players may have died or state may have changed since the execution was created.

Idempotency

Executions should be safe to replay. Avoid side effects outside of game state.

No Async

execute() is synchronous. Use persistentStorage in lifecycle hooks for async operations.