Первый скрипт вывода на орбиту вырастает до трёхсот строк, и в нём вперемешку: расписание тангажа, политика РСУ, сбор науки, запись лога и цифры конкретного контракта. Следующая миссия — копипаст всей простыни ради двух изменённых чисел.
Лечится разбиением на модули-файлы: механика отдельно, миссия отдельно.
Готовая библиотека лежит в репозитории сборки — папка
kos/: девять модулей, шаблон миссии,
проверочный прогон и telnet-клиент. Ставится копированием в <KSP>/Ships/Script/.
Присылать улучшения приветствуется.
Структура
0:/lib/
util.ks помощники: углы, векторы, азимут пуска
ctrl.ks политика РСУ и САС, возврат управления
stage.ks автостейджер
orbit.ks орбитальная механика и исполнение узлов
ascent.ks подъём: расписание тангажа, рулевой закон, тяга
deploy.ks раскрытие панелей и антенн
sci.ks сбор и передача науки
telem.ks телеметрия в CSV
hud.ks стрелки в полётном виде и надписи
0:/missions/
dalnyak.ks конкретная миссия: только цифры и порядок шагов
kOS поддерживает каталоги, путь вида 0:/lib/orbit.ks работает как в обычной системе. Одна оговорка из документации: на macOS и Linux имена файлов только строчными буквами — иначе kOS их не найдёт.
Как это подключается
RUNONCEPATH("0:/lib/util.ks").
RUNONCEPATH("0:/lib/orbit.ks").
RUNONCEPATH отличается от RUNPATH тем, что не выполнит файл повторно, если он уже загружен в этом запуске. Это ровно то, что нужно библиотеке: модуль orbit.ks может сам подключить ctrl.ks, и если миссия уже его подключила — второй раз он не выполнится.
Важное ограничение: RUNONCEPATH работает только внутри программы. Из терминала он выдаст ошибку — там каждый запуск и так компилируется заново.
Три правила, без которых развалится
1. Префиксы в именах. Все функции модуля начинаются с его буквы: u_ для util, o_ для orbit, a_ для ascent, s_ для stage, c_ для ctrl, d_ для deploy, t_ для telem, h_ для hud. В kOS одно глобальное пространство имён, и без префиксов два модуля рано или поздно объявят функцию с одним именем.
2. GLOBAL FUNCTION, а не просто FUNCTION. Иначе функция не будет видна вызывающему файлу.
3. Никаких побочных действий при загрузке. Модуль объявляет функции и значения по умолчанию — и всё. Ничего не печатает, ничем не рулит, ничего не открывает. Иначе порядок подключения начнёт влиять на поведение.
Настройки — лексиконом, а не глобальными переменными
Чтобы модуль подъёма ничего не знал о конкретной миссии, все параметры передаются одним словарём:
GLOBAL FUNCTION a_defaults {
RETURN LEXICON(
"park", 80000, "az", 90, "vVert", 60,
"hDense", 25000, "aoaLo", 5, "aoaHi", 20,
"hTwr", 22000, "twrMax", 2.3, "qMax", 0.32, "aoaAlarm", 14,
"onTick", { }
).
}
Миссия берёт умолчания и правит только нужное:
SET CFG TO a_defaults().
SET CFG["park"] TO 80000.
SET CFG["az"] TO u_launchAzimuth(8.5).
SET CFG["onTick"] TO missionTick@.
Обратные вызовы: onTick
Модулю подъёма не место в решении, что писать в лог. Поэтому он просто зовёт функцию, которую ему дали:
LOCAL onTick IS cfg["onTick"].
UNTIL SHIP:APOAPSIS > cfg["park"] {
...
onTick().
WAIT 0.
}
Символ @ после имени функции — ссылка на неё, а не вызов. Так миссия подсовывает свой обработчик, а модуль остаётся независимым. Тот же приём в o_burn(nd, onTick@).
Следующий шаг: миссия одной строкой
Разбиение на модули убирает копипаст механики, но файл миссии всё ещё описывает порядок шагов. А порядок этот выводится из самой цели: если контракт задаёт аргумент перицентра — нужна круговая парковка и ожидание точки витка, если нет — хватит одного совмещённого прожига.
Значит и порядок можно считать. Верхний модуль принимает описание целевой орбиты и собирает профиль сам:
RUNONCEPATH("0:/lib/mission.ks").
SET s TO m_defaults().
SET s["ap"] TO 7867078. // апоцентр, м
SET s["pe"] TO 3331362. // перицентр, м
SET s["inc"] TO 20.1. // наклонение, °
SET s["lan"] TO 250.8. // долгота узла (-1 = не ждать окна)
SET s["aop"] TO 29.6. // аргумент перицентра (-1 = не важен)
m_orbit(s).
Что решается без участия человека:
| Решение | На основании чего |
|---|---|
| Азимут пуска | наклонение + поправка на вращение планеты |
| Ждать ли окно пуска | задана ли долгота узла |
| Нужна ли парковка | задан ли аргумент перицентра |
| Где делать прожиг | аргумент перицентра = точка витка, где жжём |
| Поднимать ли перицентр | сравнение с текущим |
Что есть ap, а что pe |
если перепутаны местами — меняются молча |
Файл миссии сжимается до шестнадцати строк, половина из которых комментарии. Это и есть предел разумного дробления: человек задаёт цель, скрипт выводит путь.
Как выглядит файл миссии
Ни строчки механики — только цель и порядок:
SET CFG TO a_defaults().
SET CFG["az"] TO u_launchAzimuth(8.5).
SET CFG["onTick"] TO missionTick@.
sciSweep("на столе", FALSE).
IF a_run(CFG) {
d_all(). // панели и антенны
sciSweep("вышли в космос", TRUE).
o_burn(o_nodeRaiseOpposite(4757918), missionTick@).
o_burn(o_nodeRaisePeri(4591510), missionTick@).
sciSweep("целевая орбита", TRUE).
}
h_off().
c_release().
Новая миссия — копия этого файла с другими числами. Механику трогать не надо.
Что даёт разбиение
| Простыня | Модули | |
|---|---|---|
| Новая миссия | копипаст 300 строк | копия файла на 60 строк |
| Починил ошибку в стейджере | правишь в каждой копии | правишь в одном месте |
| Отладка | ищешь в общей куче | знаешь, в каком файле смотреть |
| Проверка синтаксиса | только запуском | каждый модуль отдельно |
Последнее особенно ценно вместе с kOS по telnet: пульт снаружи игры: модуль можно прогнать на площадке отдельно от остальных и увидеть все опечатки до пуска.
Подводные камни
Место на диске. У процессора CX-4181 всего 10 000 байт, у младших моделей 5 000. Библиотека туда не влезет. Но если запускать из архива (0:/…), локальный диск не нужен вовсе — файлы читаются с «земли». Для дальних аппаратов, где архив недоступен без связи, скрипты компилируют в .ksm командой COMPILE: машинный код занимает в разы меньше.
Компиляция не бесплатна. Каждый уникальный файл компилируется один раз за запуск программы и держится в памяти до её конца. Десяток мелких модулей на старте — это заметная пауза перед первой командой. Не дроби ради дробления.
Порядок подключения. Если модуль зовёт функцию из другого, тот должен быть подключён раньше. Проще всего подключать всё скопом в начале файла миссии, а в шапке каждого модуля писать, от чего он зависит.
Одно пространство имён. Глобальная переменная из модуля видна всем и может быть затёрта миссией. Префиксы обязательны и для переменных: A_CMD, T_PATH, C_RCS_UNTIL.
Кто владеет объектом. Самая коварная ошибка разбиения: при переносе кода в модуль теряется строчка, которая в простыне стояла рядом. У нас так пропал ADD nd — фабрика возвращала узел манёвра, а вешать его в план полёта было некому:
Must attach node first
At 0:/lib/orbit.ks, line 46
LOCAL dv0 IS nd:DELTAV.
Узел, не прикреплённый к плану, не отдаёт DELTAV. Компиляция проходит, ошибка вылезает в полёте — на переходе к первому прожигу.
Лечится правилом: у каждого объекта один владелец. Раз o_burn снимает узел через REMOVE, пусть он же его и вешает:
GLOBAL FUNCTION o_burn {
PARAMETER nd.
IF NOT HASNODE OR NEXTNODE <> nd { ADD nd. }
WAIT 0.
...
IF HASNODE { REMOVE nd. }
}
Что происходит, когда скрипт падает. kOS снимает все LOCK, и аппарат остаётся без управления — в вакууме он начинает медленно вращаться. Поэтому между этапами стоит держать SAS ON, а перед своим прожигом модуль сам делает SAS OFF: одновременно рулить kOS и САС не должны.
Что положить в такую библиотеку
Проверенный набор, который окупается с первой же второй миссии:
- автостейджер — по отсутствию живых двигателей, а не по выгоранию (Скрипт вывода на орбиту: разбор ошибок);
- исполнение узла — наведение, перемотка, прожиг, снятие узла;
- подъём — расписание тангажа с ограничением угла атаки;
- орбитальная математика — круговая скорость, Гоман, подъём апсид;
- политика РСУ — точечное включение вместо постоянного;
- наука — сбор и передача с оглядкой на атмосферу;
- телеметрия — CSV с ограничением частоты записи;
- визуальная отладка — стрелки потока и команды (kOS: интерфейс, графика и HUD).
Точные сигнатуры всех функций — kOS: справочник библиотеки kerbal.ru. Как это складывается в полёт от команды до орбиты — Автоматизация запуска по шагам.