Ikar BotHardShiftRoadmapContactGet
IkaScript · Script API

IkaScript — Scripting API

Ikar Bot opens its world to Kotlin. A script drives one character through a single bot object — read the world, send commands, wait for events. This is the whole public surface: every method, every model field, every event.

KotlinAPI v1One script · one characterBuilt-in editor or .jar plugin
The reference
Starter project

The example plugin

A minimal Gradle project that builds into a working .jar plugin — it attacks the nearest living mob, over and over. Replace the body of run and it's your script.

  • MyFarm.kt — the whole script, twenty lines
  • The API jars to compile against
  • ServiceLoader registration, already wired up
  • Gradle wrapper — a JDK 17 is all you need
Download the exampleZIP · 1.7 MB · Gradle + Kotlin
On this page

A script is Kotlin code that controls one character. Everything goes through the bot: L2Bot object, which is handed to the script on start and is the single entry point into the API.

L2Bot has three kinds of members:

KindFormExample
World readspropertiesbot.user.hp, bot.npcs, bot.target
Commandssuspend functions returning Booleanbot.attack(mob), bot.castSkill(1177)
Eventsa streambot.events, bot.waitEvent<…>(…)

Writing a script

Option 1 — the built-in editor

In the editor you write only the script body. The class and its metadata are generated for you:

Kotlin
override suspend fun run(bot: L2Bot) {
    bot.log("Hello, ${bot.user.name} (lvl ${bot.user.level})")
    delay(1000)
}

These imports are already in place — don't write them:

com.ikar.script.api.*
com.ikar.script.api.model.*
com.ikar.script.protocol.*
kotlinx.coroutines.*

You can also override onStop().

This import list is fixed: the body is inserted inside a class, and imports can't be declared there. The whole API, delay, launch, coroutineScope and waitEvent are available right away; Flow operators (filterIsInstance, collect) are not — for those use the jar option.

Option 2 — a .jar plugin

Implement the IkaScript interface in your own project, build a jar and register the implementation via ServiceLoader — put a META-INF/services/com.ikar.script.api.IkaScript file in the jar containing the fully qualified name of your class.

Kotlin
class MyFarm : IkaScript {

    override val meta = ScriptMeta(id = "my-farm", name = "My Farm")

    override suspend fun run(bot: L2Bot) {
        while (true) {
            val mob = bot.npcs.nearest { it.attackable && !it.dead }
            if (mob != null && bot.user.inRange(mob, 800)) bot.attack(mob)
            delay(500)
        }
    }

    override fun onStop() { /* cleanup, optional */ }
}

Drop the jar into the scripts directory (data/scripts by default) or open it from the application manually.

The IkaScript interface

MemberTypeDescription
metaScriptMetaScript metadata.
run(bot: L2Bot)suspend funThe script body. All of your logic lives here.
onStop()funCalled when the script stops. Does nothing by default.

The ScriptMeta class

FieldTypeDefaultDescription
idStringScript identifier.
nameStringDisplay name.
versionString"1.0"Version of the script itself; does not affect loading.
apiVersionIntcurrent API versionVersion of the API contract the plugin was built against.

A plugin whose apiVersion does not match the application's API version is not loaded. The current version is ScriptApi.VERSION = 1; the field fills itself in, you never set it by hand.

Three rules that save hours of debugging

1. Entities are a snapshot, not a live reference

Every read of bot.user / bot.npcs / bot.drops returns the current picture of the world. But the object you get is a snapshot taken at read time — it never updates itself. After a delay or any command its data is stale.

Kotlin
// Wrong: mob is read once, so its hp is frozen from here on
val mob = bot.npcs.nearest { it.attackable }!!
while (mob.hp > 0) {          // this condition will never change
    bot.attack(mob)
    delay(500)
}

// Right: re-read the world on every iteration
while (true) {
    val mob = bot.npcs.nearest { it.attackable && !it.dead } ?: break
    bot.attack(mob)
    delay(500)
}

2. Compare entities by oid, not with ==

Objects are rebuilt on every world update, so == (reference equality) returns false even for the very same mob.

Kotlin
if (bot.target?.oid == mob.oid) {  }   // right
if (bot.target == mob) {  }            // this will not work

3. true doesn't always mean "it worked"

Some commands wait for the outcome in game, others just send the action without waiting for confirmation — those always return true. What a specific method's return value means is spelled out in the "Returns" column on the Commands page.

There is no auto-retry: if a command returned false, retrying is the script's decision.

id or oid — commands accept both

  • id (template id) is a type: a mob id, an item's itemId, a skillId. The number you see in game and in guides. Every orc on the map shares it.
  • oid (object id) is a specific instance: this orc, this item in your bag, this drop on the ground. Issued by the server, alive as long as the object is, and carried in event fields.

A plainly named method (setTarget, attack, pickup, useItem, destroyItem, openDialog, …) first looks the number up as a template id among nearby objects. If nothing matches, it treats the number as an object id and sends it as is.

Kotlin
bot.setTarget(20001)       // mob type → nearest living mob of that type
bot.setTarget(mob.oid)     // object id → exactly this mob
bot.setTarget(mob)         // entity → no guessing at all
bot.destroyItem(57, 1000)  // 57 = adena (template id)
bot.pickup(57)             // nearest adena on the ground

A collision is impossible: the server issues object ids starting at 0x10000000, while template ids are in the thousands. A number that matches nothing visible is not discarded: an oid from a just-received event can be ahead of the world snapshot, so it goes to the game as is.

When you need to be unambiguous:

FormWhat it does
entity overload — pickup(drop), destroyItem(item, n), openDialog(npc)Exactly this object. The most reliable path.
*ByOidpickupByOid(oid), setTargetByOid(oid), destroyItemByOid(oid, n)Strictly an object id.
*ByTypesetTargetByType(npcId), useItemByType(itemId), destroyItemByType(itemId, n)Strictly a template id.

Stopping a script

Stopping interrupts run at the nearest waiting point — a delay, or any suspend command. You don't need to handle an infinite while (true) yourself.

If something has to be cleaned up afterwards (drop the target, stand up, switch automation back on), use try/finally or onStop():

Kotlin
override suspend fun run(bot: L2Bot) {
    bot.disableAutomation()
    try {
        while (true) {  }
    } finally {
        bot.enableAutomation()
    }
}

Walking in progress (moveTo, moveToByGeo) is aborted when the script stops — the character won't keep following a route with no script behind it.

Scripts and built-in automation

A script and the built-in bot (the one you configure in the application) run in parallel and independently: automation never cancels the script's commands, and the script's commands never break automation.

But there is only one character. With automation on, it will be steering the character towards its own goals at the same time — during a long walk or a complex scenario that looks like the character is torn between two masters. The script acts as the director here:

Kotlin
bot.disableAutomation()
bot.moveToByGeo(x, y, z, timeoutMs = 0)
bot.enableAutomation()

Where to start

Kotlin
override suspend fun run(bot: L2Bot) {
    bot.log("Started: ${bot.user.name}")

    while (true) {
        val me = bot.user

        if (me.dead) {
            bot.log("Dead — waiting")
            delay(3000)
            continue
        }

        // pick up my own drop at my feet
        bot.drops.nearest { it.isMine && it.distToSelf <= 200 }?.let { bot.pickup(it) }

        // attack the nearest mob
        val mob = bot.npcs.nearest { it.attackable && !it.dead && it.distToSelf <= 1500 }
        if (mob != null) {
            bot.log("Attacking ${mob.name} at ${mob.distToSelf} units")
            bot.attack(mob)
            bot.waitEvent<ScriptEvent.Died>(15_000) { it.oid == mob.oid }
        }

        delay(500)
    }
}

Next: Commands, Models, Events, Examples.