Ikar BotHardShiftПланыКонтактыСкачать
IkaScript · API скриптов

IkaScript — API скриптов

Ikar Bot открывает свой мир для Kotlin. Скрипт управляет одним персонажем через единственный объект bot — читает мир, отправляет команды, ждёт события. Здесь описана вся публичная часть API: каждый метод, каждое поле модели, каждое событие.

KotlinAPI v1Один скрипт · один персонажВстроенный редактор или плагин .jar
Справочник
Стартовый проект

Пример плагина

Минимальный Gradle-проект, который собирается в рабочий плагин .jar — атакует ближайшего живого моба по кругу. Замените тело run, и это уже ваш скрипт.

  • MyFarm.kt — весь скрипт, двадцать строк
  • Jar-ы API, против которых идёт сборка
  • Регистрация через ServiceLoader — уже прописана
  • Gradle wrapper — из своего нужен только JDK 17
Скачать примерZIP · 1,7 МБ · Gradle + Kotlin
На этой странице

Скрипт — это Kotlin-код, который управляет одним персонажем. Всё общение с игрой идёт через объект bot: L2Bot: он передаётся в скрипт при запуске и является единственной точкой входа в API.

У L2Bot три вида членов:

ВидФормаПример
Чтение мирасвойстваbot.user.hp, bot.npcs, bot.target
Командыsuspend-функции, возвращают Booleanbot.attack(mob), bot.castSkill(1177)
Событияпотокbot.events, bot.waitEvent<…>(…)

Как написать скрипт

Вариант 1 — встроенный редактор

В редакторе пишется только тело скрипта. Класс и метаданные создаются автоматически:

Kotlin
override suspend fun run(bot: L2Bot) {
    bot.log("Привет, ${bot.user.name} (ур. ${bot.user.level})")
    delay(1000)
}

Импорты уже подключены — писать их не нужно:

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

Можно также переопределить onStop().

Этот список импортов фиксирован: тело вставляется внутрь класса, а импорт там объявить нельзя. Всё API, delay, launch, coroutineScope и waitEvent доступны сразу; операторы Flow (filterIsInstance, collect) — нет, для них нужен вариант с jar.

Вариант 2 — плагин .jar

Реализуй интерфейс IkaScript в своём проекте, собери jar и зарегистрируй реализацию через ServiceLoader — положи в jar файл META-INF/services/com.ikar.script.api.IkaScript, содержащий полное имя твоего класса.

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() { /* очистка, опционально */ }
}

Готовый jar кладётся в каталог скриптов (по умолчанию data/scripts) или открывается из приложения вручную.

Интерфейс IkaScript

ЧленТипОписание
metaScriptMetaМетаданные скрипта.
run(bot: L2Bot)suspend funТело скрипта. Здесь живёт вся логика.
onStop()funВызывается при остановке. По умолчанию ничего не делает.

Класс ScriptMeta

ПолеТипПо умолчаниюОписание
idStringИдентификатор скрипта.
nameStringОтображаемое имя.
versionString"1.0"Версия самого скрипта, на загрузку не влияет.
apiVersionIntтекущая версия APIВерсия контракта API, против которого собран плагин.

Плагин, у которого apiVersion не совпадает с версией API приложения, не загружается. Текущая версия — ScriptApi.VERSION = 1; поле заполняется само, руками его задавать не нужно.

Три правила, которые экономят часы отладки

1. Сущности — это снимок, а не живая ссылка

Каждое обращение к bot.user / bot.npcs / bot.drops возвращает актуальную картину мира. Но полученный объект — снимок на момент чтения: он не обновляется сам. После delay или любой команды данные в нём устарели.

Kotlin
// Неправильно: mob прочитан один раз, дальше его hp «заморожено»
val mob = bot.npcs.nearest { it.attackable }!!
while (mob.hp > 0) {          // условие никогда не изменится
    bot.attack(mob)
    delay(500)
}

// Правильно: перечитываем мир на каждой итерации
while (true) {
    val mob = bot.npcs.nearest { it.attackable && !it.dead } ?: break
    bot.attack(mob)
    delay(500)
}

2. Сравнивай сущности по oid, а не через ==

Объекты пересоздаются на каждом обновлении мира, поэтому == (ссылочное равенство) даст false даже для одного и того же моба.

Kotlin
if (bot.target?.oid == mob.oid) {  }   // правильно
if (bot.target == mob) {  }            // так работать не будет

3. true не всегда значит «получилось»

Часть команд ждёт исхода в игре, часть просто отправляет действие и не ждёт подтверждения — такие всегда возвращают true. Что именно означает возврат конкретного метода, указано в колонке «Возврат» на странице Команды.

Автоповтора нет: если команда вернула false, решение о повторе принимает скрипт.

id или oid — команды принимают оба

  • id (template-id) — это тип: id моба, itemId вещи, skillId. Тот самый номер, что виден в игре и в гайдах. Одинаков у всех орков на карте.
  • oid (object id) — это конкретный экземпляр: вот этот орк, вот эта вещь в сумке, вот этот дроп на земле. Выдаётся сервером, живёт пока объект существует, приходит в полях событий.

Метод с простым именем (setTarget, attack, pickup, useItem, destroyItem, openDialog, …) сначала ищет переданное число как template-id среди объектов рядом. Не нашёл такого — считает число object id и отправляет как есть.

Kotlin
bot.setTarget(20001)       // тип моба → ближайший живой моб этого типа
bot.setTarget(mob.oid)     // object id → ровно этот моб
bot.setTarget(mob)         // сущность → без всякого угадывания
bot.destroyItem(57, 1000)  // 57 = адена (template-id)
bot.pickup(57)             // ближайшая адена на земле

Коллизия невозможна: object id сервер выдаёт начиная с 0x10000000, а template-id — это тысячи. Число, которого нет среди видимых объектов, команда не отбрасывает: oid из только что полученного события может опережать снимок мира, поэтому он уходит в игру как есть.

Когда нужна однозначность:

ФормаЧто делает
перегрузка по сущности — pickup(drop), destroyItem(item, n), openDialog(npc)Ровно этот объект. Самый надёжный путь.
*ByOidpickupByOid(oid), setTargetByOid(oid), destroyItemByOid(oid, n)Строго object id.
*ByTypesetTargetByType(npcId), useItemByType(itemId), destroyItemByType(itemId, n)Строго template-id.

Остановка скрипта

Остановка прерывает run на ближайшей точке ожидания — на delay, на любой suspend-команде. Бесконечный while (true) останавливать отдельно не нужно.

Если после скрипта нужно что-то прибрать (снять цель, встать, включить обратно автоматизацию) — используй try/finally или onStop():

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

Идущая ходьба (moveTo, moveToByGeo) при остановке скрипта прерывается — персонаж не продолжит идти по маршруту без скрипта.

Скрипт и встроенная автоматизация

Скрипт и встроенный бот (тот, что настраивается в приложении) работают параллельно и независимо: команды скрипта не отменяются автоматизацией, а автоматизация не ломается от команд скрипта.

Но персонаж один. Если автоматизация включена, она будет одновременно вести бота к своим целям — при длинной ходьбе или сложном сценарии это выглядит как метания. Скрипт здесь выступает «режиссёром»:

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

С чего начать

Kotlin
override suspend fun run(bot: L2Bot) {
    bot.log("Старт: ${bot.user.name}")

    while (true) {
        val me = bot.user

        if (me.dead) {
            bot.log("Мёртв — жду")
            delay(3000)
            continue
        }

        // подобрать свой дроп под ногами
        bot.drops.nearest { it.isMine && it.distToSelf <= 200 }?.let { bot.pickup(it) }

        // атаковать ближайшего моба
        val mob = bot.npcs.nearest { it.attackable && !it.dead && it.distToSelf <= 1500 }
        if (mob != null) {
            bot.log("Атакую ${mob.name} на ${mob.distToSelf} ед.")
            bot.attack(mob)
            bot.waitEvent<ScriptEvent.Died>(15_000) { it.oid == mob.oid }
        }

        delay(500)
    }
}

Дальше — Команды, Модели, События, Примеры.