Разработка дополнений DFHack
WIP. Статья будет дописываться со временем
Главные особенности, основы программирования скриптов и плагинов для DFHack
Описание объектов дополнений
Всего 2 класса дополнений: плагины и скрипты Lua/Ruby. Для выполнения определённых действий "От и до" достаточно использовать скрипты. Плагины используются для расширения игры, или тогда, когда невозможно добиться результата скриптами.
Lua/Ruby скрипты
Что могут делать скрипты по умолчанию?
- Манипулировать памятью игры
- Читать и писать файлы в файловой системе
- Читать некоторую системную информацию
- Создавать TCP сервер и быть TCP клиентом, отправлять и получать данные по сети (очень ограниченно и в блокирующем режиме)
- Регистрировать ряд событий через Eventful
- Дополнять игровые экраны (например, экран продолжения игры)
- Запускать новые процессы в блокирующем режиме (
os.execute
), получать их вывод в переменные (io.popen
)
Что не могут делать скрипты?
- Посылать и принимать данные по UDP, без middleware
- Дополнять функционал самого ядра DFHack, ограничиваясь встроенным функционалом Lua и утилитами DFHack под Lua
- Создавать дополнительные экземпляры интерпретатора (что могут делать плагины)
Плагины
Особенности плагинов
- Могут быть включены и отключены, не требуют ручного вызова каждый раз
- Пишутся на языке C++
- Могут использовать методы, функции, рутины на каждом игровом тике
- Могут внедрять функционал, который невозможно никак запрограммировать посредством Ruby или Lua
- Могут быть внедрены в интерфейс
- Расширенное управление графикой и интерфейсом (например, индикатор настроения в правом нижнем углу, манипулятор профессий
u→l
,autochop
и прочие расширения интерфейса)
- Расширенное управление графикой и интерфейсом (например, индикатор настроения в правом нижнем углу, манипулятор профессий
Недостатки плагинов
- Требуют полной сборки DFHack из исходников
- Официальной документации почти нет
- Если Lua/Ruby скрипты могут быть быстро исправлены при выявлении ошибок или дополнены, то плагины требуют пересборки
Основы разработки дополнений
Среды для разработки
Для написания скриптов не требуется особого инструментария, подойдёт даже простой блокнот
Lua
Для Lua немного сред, где есть интеллектуальный анализ, лучший вариант — JetBrains IntelliJ Idea c плагином EmmyLua.
Важно отметить, что игровые переменные не будут в авто-завершении кода. К примеру, написание df.global.
не выведет список доступных членов класса df.global
. Нужно руководствоваться codegen.out.xml
или анализировать память через devel/query
Ruby
Платный редактор RubyMine от JetBrains — самый комплексный IDE для разработки на языке Ruby.
Общие редакторы для Lua и Ruby
Подойдёт любой текстовый редактор или редактор кода. Варианты:
- VSCodium, VS Code
- Atom
- Notepad++
- Vim
- Emacs
Среды для разработки плагинов
Плагины комплексные, пишутся на языке C++, который сам по себе непростой язык. Плагины требуют сборки, исходный код требует особых директив, включений include
для каждого вида игровых данных, в отличие от Lua скриптов, которые могут обращаться ко всем доступным переменным df
напрямую.
Идеально использовать Visual Studio версии 2015 или 2017, с установленной поддержкой Windows XP v140. Так как C++ достаточно комплексный, для эффективной разработки рекомендуется использовать именно IDE, а не текстовые редакторы, даже те, которые имеют подсветку синтаксиса.
Другой вариант: CLion от JetBrains. Требует особой настройки при загрузке исходников DFHack. При неполной настройке не будет доступна система интеллектуального ввода, только базовая подсветка синтаксиса (TODO: нужно определить эти настройки)
Некоторые понятия
Вектор — одномерный массив из одних и тех же данных. Например, вектор юнитов, вектор цивилизаций. В Lua векторы это почти как таблица table
, но, вдобавок, для них разработчиками DFHack разработаны специальные методы.
Скрипт — файл Lua или Ruby, выполняющий строгий алгоритм. Запускается и завершается
Middleware — посторонняя программа, позволяющая взаимодействовать третьим сервисам с DFHack. Например, Python скрипт, обеспечивающий связь с игрой посредством protobuf
.
Persistent — данные, сохранённые в файл dfhack-legacy-data.dat
, используя dfhack.persistent
Откуда брать данные
Главный источник — codegen.out.xml
, получаемый после сборки DFHack. Этот файл расположен по пути library/include/df
и занимает больше 3.6 мегабайт
Программирование скриптов
Далее пойдут полезные сниппеты, часто используемые функции
Манипуляции с данными
Ниже представлены сниппеты для работы с данными
Кодирование в Base64
local bs = { [0] = 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/', } local function base64(s) local byte, rep = string.byte, string.rep local pad = 2 - ((#s-1) % 3) s = (s..rep('\0', pad)):gsub("...", function(cs) local a, b, c = byte(cs, 1, 3) return bs[a>>2] .. bs[(a&3)<<4|b>>4] .. bs[(b&15)<<2|c>>6] .. bs[c&63] end) return s:sub(1, #s-pad) .. rep('=', pad) end
HEX
---Из HEX в строку function string.fromhex(str) return (str:gsub('..', function (cc) return string.char(tonumber(cc, 16)) end)) end ---Из строки в HEX-строку function string.tohex(str) return (str:gsub('.', function (c) return string.format('%02X', string.byte(c)) end)) end
Файловая система
DFHack и Lua дают расширенный доступ к файловой системе. Через скрипты можно создавать файлы, папки, а также их удалять.
Важно отметить, что в Lua нет поддержки кодировок; чтение скриптов и файлов, а также вся запись, происходят в ANSI. Кодирование результирующих файлов в UTF-8 и в другие кодировки следует производить в других программах. Современные текстовые редакторы (Notepad++ и другие) поддерживают автоматическое определение кодировки.
Строку ТЕСТ
Lua будет видеть как ТЕСТ
Открытие файлов
file = io.open(ПутьКФайлу [, режим])
Режимы открытия файлов. | |
---|---|
Режим | Описание |
r | Только чтение, стандартный режим открытия существующих файлов |
w | Режим записи: создаёт новый файл или полностью перезаписывает существующий файл |
a | Режим дополнения: создаёт новый файл или открывает существующий для дополнения, не удаляя существующие данные |
r+ | Режим обновления, чтение и запись для существующего файла |
w+ | Режим обновления, вся предыдущая информация стирается. Либо создаётся новый файл с разрешениями на чтение и запись |
a+ | Режим дополнения с чтением для существующего файла, запись разрешена только в конец файла |
Сохранение файлов
io.output(dfhack.getHackPath() .. "test.txt") --Файл будет по пути /hack/text.txt io.write("test data") --В файл будет записана строка в ANSI io.close() --Закрытие файла
local file = io.open(dfhack.getHackPath() .. "test2.txt", "w") file:write("test 2") file:close()
Открытие файлов
local file_path = dfhack.getHackPath() .. "test.txt" --./hack/test.txt local _file, data_from_file if dfhack.filesystem.isfile(file_path) then --проверка, существует ли файл _file = io.open(file_path, "r") --Открыть только для чтения data_from_file = _file:read "*a" --прочитать весь файл целиком _file:close() --закрыть файл, он больше не нужен print(data_from_file --вывести в stdout else qerror("File not found") end
Сериализация и десериализация JSON
Разработчики DFHack включили поддержку JSON в Lua, позволяя читать и записывать данные в файлы JSON.
Ограничения: доступна только сериализация некоторых объектов Lua, userdata
, function
невозможно сохранить.
Полученную JSON-строку можно сохранить в файл при помощи io.write
Лучшие практики применения JSON в скриптах:
- Настройки
- Текстовые сообщения для вывода в консоль
- Сохранение в
persistent
определённого мира
local json=require"json" --Загрузка модуля JSON local t = { { ["key"] = "color", ["value"] = "RED" }, { ["index"] = 12345, --Числа ["value"] = true --Булево } } print(json.encode(t)) --Красивый вывод с отступами print(json.encode(t, {pretty=false})) --Компактно
Вывод при запуске этого кода:
[ { "key": "color", "value": "RED" }, { "index": 12345, "value": true } ] [{"key":"color","value":"RED"},{"index":12345,"value":true}]
Локализация вывода скриптов
WIP
Крайне рекомендуется делать вывод в 7-битном ASCII
В Lua есть серьёзное ограничение — нет полноценной работы с UTF-8. Манипуляции со строками, такие как string.gsub
и другие, проходят в однобайтовом режиме, что приведёт к логическим ошибкам при обработке строк, написанных в UTF-8.
Варианты работы с локализованным текстом:
- Рискованный — выводить данные непосредственно от UTF-8. В случае простого вывода, без дополнительных манипуляций, этого может быть достаточно
- Русские буквы занимают 2 байта, а эмодзи занимают 4+ байта
- Локализация в middleware — переводимый текст выводится из DFHack в сериализованном виде через
stdout
в кодировке ANSI, с наличием уникальных ключей локализации, составляющих сообщение. Впоследствии, из этих данных собирается переведённое сообщение
Сетевые возможности, протокол TCP
WIP
В DFHack есть ограниченная поддержка TCP: можно создать сервер и клиент.
Что пока что не может быть реализовано?
- Создание новых наборов изображений: тех, которые изображаются на артефактах, фигурках, статуях, огранках и другом.
- Крайне сложная система их генерации, много переменных не выявлено
- Создание армий и управляемых вторжений
- Наличие неизвестных переменных в памяти, назначение которых неизвестно разработчикам DFHack
- Остальное то, что неизвестно разработчикам DFHack. Многие переменные просто-напросто не раскрыты, а их назначение неизвестно. Чаще всего это числовые переменные и векторы с числами