IkaScript — API скриптов
Ikar Bot открывает свой мир для Kotlin. Скрипт управляет одним персонажем через единственный объект bot — читает мир, отправляет команды, ждёт события. Здесь описана вся публичная часть API: каждый метод, каждое поле модели, каждое событие.
Пример плагина
Минимальный Gradle-проект, который собирается в рабочий плагин .jar — атакует ближайшего живого моба по кругу. Замените тело run, и это уже ваш скрипт.
- MyFarm.kt — весь скрипт, двадцать строк
- Jar-ы API, против которых идёт сборка
- Регистрация через ServiceLoader — уже прописана
- Gradle wrapper — из своего нужен только JDK 17
На этой странице
Скрипт — это Kotlin-код, который управляет одним персонажем. Всё общение с игрой идёт через объект bot: L2Bot: он передаётся в скрипт при запуске и является единственной точкой входа в API.
У L2Bot три вида членов:
| Вид | Форма | Пример |
|---|---|---|
| Чтение мира | свойства | bot.user.hp, bot.npcs, bot.target |
| Команды | suspend-функции, возвращают Boolean | bot.attack(mob), bot.castSkill(1177) |
| События | поток | bot.events, bot.waitEvent<…>(…) |
Как написать скрипт
Вариант 1 — встроенный редактор
В редакторе пишется только тело скрипта. Класс и метаданные создаются автоматически:
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, содержащий полное имя твоего класса.
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
| Член | Тип | Описание |
|---|---|---|
meta | ScriptMeta | Метаданные скрипта. |
run(bot: L2Bot) | suspend fun | Тело скрипта. Здесь живёт вся логика. |
onStop() | fun | Вызывается при остановке. По умолчанию ничего не делает. |
Класс ScriptMeta
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
id | String | — | Идентификатор скрипта. |
name | String | — | Отображаемое имя. |
version | String | "1.0" | Версия самого скрипта, на загрузку не влияет. |
apiVersion | Int | текущая версия API | Версия контракта API, против которого собран плагин. |
Плагин, у которого apiVersion не совпадает с версией API приложения, не загружается. Текущая версия — ScriptApi.VERSION = 1; поле заполняется само, руками его задавать не нужно.
Три правила, которые экономят часы отладки
1. Сущности — это снимок, а не живая ссылка
Каждое обращение к bot.user / bot.npcs / bot.drops возвращает актуальную картину мира. Но полученный объект — снимок на момент чтения: он не обновляется сам. После delay или любой команды данные в нём устарели.
// Неправильно: 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 даже для одного и того же моба.
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 и отправляет как есть.
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) | Ровно этот объект. Самый надёжный путь. |
*ByOid — pickupByOid(oid), setTargetByOid(oid), destroyItemByOid(oid, n) | Строго object id. |
*ByType — setTargetByType(npcId), useItemByType(itemId), destroyItemByType(itemId, n) | Строго template-id. |
Остановка скрипта
Остановка прерывает run на ближайшей точке ожидания — на delay, на любой suspend-команде. Бесконечный while (true) останавливать отдельно не нужно.
Если после скрипта нужно что-то прибрать (снять цель, встать, включить обратно автоматизацию) — используй try/finally или onStop():
override suspend fun run(bot: L2Bot) {
bot.disableAutomation()
try {
while (true) { … }
} finally {
bot.enableAutomation()
}
}Идущая ходьба (moveTo, moveToByGeo) при остановке скрипта прерывается — персонаж не продолжит идти по маршруту без скрипта.
Скрипт и встроенная автоматизация
Скрипт и встроенный бот (тот, что настраивается в приложении) работают параллельно и независимо: команды скрипта не отменяются автоматизацией, а автоматизация не ломается от команд скрипта.
Но персонаж один. Если автоматизация включена, она будет одновременно вести бота к своим целям — при длинной ходьбе или сложном сценарии это выглядит как метания. Скрипт здесь выступает «режиссёром»:
bot.disableAutomation()
bot.moveToByGeo(x, y, z, timeoutMs = 0)
bot.enableAutomation()С чего начать
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)
}
}