Ikar BotHardShiftRoadmapContactoDescargar
IkaScript · Script API

IkaScript — Scripting API

Ikar Bot abre su mundo a Kotlin. Un script maneja un personaje a través de un único objeto bot — lee el mundo, envía comandos, espera eventos. Esta es toda la superficie pública: cada método, cada campo de modelo, cada evento.

KotlinAPI v1Un script · un personajeEditor integrado o plugin .jar
La referencia
Proyecto de inicio

El plugin de ejemplo

Un proyecto Gradle mínimo que compila en un plugin .jar funcional — ataca al mob vivo más cercano, una y otra vez. Sustituye el cuerpo de run y ya es tu script.

  • MyFarm.kt — el script entero, veinte líneas
  • Los jars de la API contra los que compilar
  • Registro con ServiceLoader, ya montado
  • Wrapper de Gradle — solo necesitas un JDK 17
Descargar el ejemploZIP · 1,8 MB · Gradle + Kotlin

La referencia de la Script API está por ahora solo en inglés — el resto del sitio sigue tu idioma.

En esta página

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.

You do not have to set the project up yourself: a ready one sits next to this reference — ikar-script-example.zip. It holds a working plugin, the ServiceLoader registration file, the Gradle setup with a wrapper, and libs/ with the API jars it compiles against. Unpack it, replace the body of run, then gradlew jar.

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
idString—Script identifier.
nameString—Display 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.
*ByOid — pickupByOid(oid), setTargetByOid(oid), destroyItemByOid(oid, n)Strictly an object id.
*ByType — setTargetByType(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, Changelog.