Разработка дополнений DFHack

Материал из Dwarf Fortress Wiki
Перейти к навигацииПерейти к поиску

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

Взято отсюда

Кодирование Base64 в Lua
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 и обратно
---Из 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+ Режим дополнения с чтением для существующего файла, запись разрешена только в конец файла

Сохранение файлов

Сохранение, вариант 1
io.output(dfhack.getHackPath() .. "test.txt") --Файл будет по пути /hack/text.txt
io.write("test data") --В файл будет записана строка в ANSI
io.close() --Закрытие файла
Сохранение, вариант 2
local file = io.open(dfhack.getHackPath() .. "test2.txt", "w")
file:write("test 2")
file:close()

Открытие файлов

Чтение содержимого файла, вариант 1
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 определённого мира
Сериализация Lua таблицы в JSON
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.

Варианты работы с локализованным текстом:

  1. Рискованный — выводить данные непосредственно от UTF-8. В случае простого вывода, без дополнительных манипуляций, этого может быть достаточно
    • Русские буквы занимают 2 байта, а эмодзи занимают 4+ байта
  2. Локализация в middleware — переводимый текст выводится из DFHack в сериализованном виде через stdout в кодировке ANSI, с наличием уникальных ключей локализации, составляющих сообщение. Впоследствии, из этих данных собирается переведённое сообщение

Сетевые возможности, протокол TCP

WIP

В DFHack есть ограниченная поддержка TCP: можно создать сервер и клиент.

Пример: отправка данных через консоль DFHack

Что пока что не может быть реализовано?

  • Создание новых наборов изображений: тех, которые изображаются на артефактах, фигурках, статуях, огранках и другом.
    • Крайне сложная система их генерации, много переменных не выявлено
  • Создание армий и управляемых вторжений
    • Наличие неизвестных переменных в памяти, назначение которых неизвестно разработчикам DFHack
  • Остальное то, что неизвестно разработчикам DFHack. Многие переменные просто-напросто не раскрыты, а их назначение неизвестно. Чаще всего это числовые переменные и векторы с числами