Перейти к содержимому

ProgramCrafter

Пользователи
  • Публикации

    245
  • Зарегистрирован

  • Посещение

  • Победитель дней

    41

Записи блога, опубликованные пользователем ProgramCrafter

  1. ProgramCrafter
    Уже давно в майнкрафте существует задача: создать библиотеку или программу, которая могла бы отрисовать произвольную картинку. Это нужно в самых разных сценариях: чтобы написать веб-браузер (или, например, приложение-клиент соцсети), показать на здании свой логотип, или для журнального столика в отеле.
     
    ———
     
    Что же мешает в OpenComputers просто считать данные картинки и её отрисовать?
    Разнообразие форматов и настроек. На многих сайтах можно одновременно найти PNG, WebP и JPEG; последние два формата подразумевают сжатие с возможными потерями и в этом отношении очень настраиваются. Соответственно, программе надо с десяток разных модулей/путей кода, чтобы декодировать что угодно — или придётся зависеть от какого-нибудь внешнего конвертера изображений. Разрешение мониторов OC не соответствует размерам стандартных картинок. У мониторов третьего уровня 160x50 знакомест (которые разбиваются на два квадратика в высоту) и всего 256 цветов в палитре. Декодирование целой картинки через виртуальную машину Lua подтормаживает (а без декодирования не получится её даже уменьшить), особенно когда приходится делать паузы для сборщика мусора. Постоянная замена цветов и отрисовка по одному пикселю тормозит уже за счёт ограничений видеокарты в моде. Пункту 4 можно противодействовать с помощью DoubleBuffering от @ECS, пункт 2 требует какого-то алгоритма уменьшения картинки (если же она слишком мала, то мы можем покрутить разрешение монитора). Что же касается первого и третьего... сразу напрашивается идея создать внешнюю библиотеку, которая всем этим бы занималась.
     
    На ум приходят несколько вариантов, на чём её можно написать и как интегрировать:
    Rust (как более безопасный и потенциально быстрый язык) скомпилировать в .dll/.so и подключить к OC, примерно как нативный вариант Lua; Того же подключить из machine.lua; На Scala (тот же язык, на котором написан OpenComputers) сделать патч к самому OpenComputers; ...
    Уже известно, что первый вариант вызывает мучения (https://computercraft.ru/blogs/entry/666-profiliruem-programmy-pod-oc/) и отсутствие персистентности у компьютеров. Пользователи, очевидно, будут очень рады тому, что программы надо писать из расчёта «всё будет выключаться в произвольные моменты».
     
     
    Поэтому сейчас рассмотрим, как пойти по третьему пути; для простоты компиляции возьмём испытуемым Ocelot Desktop, понимая, что он не очень сильно отличается от OpenComputers. Оказывается, у нас уже есть пример компонента, работающего с данными -- “data card”; основной файл этой карточки можно найти на https://gitlab.com/cc-ru/ocelot/ocelot-brain/-/blob/master/src/main/scala/totoro/ocelot/brain/entity/DataCard.scala.

    Пример
    ... object DataCard { val SecureRandomInstance: ThreadLocal[SecureRandom] = new ThreadLocal[SecureRandom]() { override def initialValue: SecureRandom = SecureRandom.getInstance("SHA1PRNG") } class Tier1 extends DataCard { ... override def tier: Tier = Tier.One @Callback(direct = true, limit = 32, doc = """function(data:string):string -- Applies base64 encoding to the data.""") def encode64(context: Context, args: Arguments): Array[AnyRef] = { result(Base64.encodeBase64(check(context, args))) } ... Сразу замечаем, что функции, экспортируемые для использования в Lua, собраны в одном классе и отмечены декоратором @Callback; кроме того, в структуре указываются ещё характеристики компонента (название, «поставщик», уровень и тому подобное).
     
    Программируем сами?..
     
    Подумаем теперь над интерфейсом нашей карточки:
    decode(массив байт) —> обёртка над картинкой (таблица? объект Scala?) обёртка —> ширина, высота обёртка —> получить цвет конкретного пикселя. Раз у нас поддерживается любой формат изображений, то здесь будут четыре канала — R, G, B, A; возможно, методы для первичной обработки изображения? Например, масштабирование.  
    В принципе, этого достаточно, чтобы начать программировать, но попробуем сделать по-другому: попросим нейросеть написать код за нас.
     
    В рассуждениях можно будет увидеть, как DeepSeek сомневается, добавлять ли собственно декодирование PNG и других форматов или сделать лишь какой-то сырой, и в итоге он останавливается на втором варианте:

    Не вполне понятно, в каком именно порядке отдаются цветовые каналы, но это похоже на правду... для какого-то сырого формата, где Lua хватило бы простого выбора байт из строки. Что ж, не будем сдаваться, запросим декодирование популярных форматов.
     
    К сожалению, там модель начинает путаться в порядке каналов и в точных импортах, но всё же выдаёт осмысленный код:
    package totoro.ocelot.brain.entity // Core image processing import com.sksamuel.scrimage.ImmutableImage import com.sksamuel.scrimage.nio.ImageSource import com.sksamuel.scrimage.metadata.ImageMetadata import com.sksamuel.scrimage.implicits._ // For Scala syntactic sugar :cite[2] // Format-specific readers (explicit safe imports) import com.sksamuel.scrimage.webp.WebpWriter import com.sksamuel.scrimage.formats.{GifWriter, JpegWriter, PngWriter} // Error handling import scala.util.{Try, Success, Failure} import totoro.ocelot.brain.entity.machine.{AbstractValue, Arguments, Callback, Context} class ImageDecoder extends /* ... existing entity setup ... */ { @Callback(direct = true, doc = """function(data:string):userdata -- Decodes common formats (PNG/WebP/JPEG/GIF) or raw RGBA""") def decode(context: Context, args: Arguments): Array[AnyRef] = { val data = args.checkByteArray(0) // 1. Attempt standard format decoding via Scrimage :cite[1]:cite[3] Try(ImmutableImage.loader().fromBytes(data)) match { case Success(scrimageImg) => // Convert Scrimage image to raw RGBA format for consistency val rgba = scrimageImg.bytes(PngWriter.NoCompression).take(8) ++ // Store metadata scrimageImg.argbFlatMap(p => Array( ((p >> 16) & 0xFF).toByte, // R ((p >> 8) & 0xFF).toByte, // G (p & 0xFF).toByte, // B ((p >> 24) & 0xFF).toByte // A )) result(new ImageUserdata(scrimageImg.width, scrimageImg.height, rgba)) case Failure(_) => // 2. Fallback to raw format if standard decoding fails if (data.length < 8) throw new IllegalArgumentException("Invalid header") val width = java.nio.ByteBuffer.wrap(data.slice(0, 4)).getInt val height = java.nio.ByteBuffer.wrap(data.slice(4, 8)).getInt if (8 + width*height*4 > data.length) throw new IllegalArgumentException("Invalid pixel data") result(new ImageUserdata(width, height, data.slice(8, data.length))) } } }
    Программируем сами, да
     
    Вооружимся компилятором (для Scala это sbt) и допишем код до нормального состояния. Дадим сначала карточке весёлое название и имя вендора:
    class ImageCard extends Entity with Environment with DeviceInfo with Tiered { override val node: Node = Network.newNode(this, Visibility.Neighbors) .withComponent("imagine", Visibility.Neighbors) .create() private final lazy val deviceInfo = Map( DeviceAttribute.Class -> DeviceClass.Processor, DeviceAttribute.Description -> "Image decoder card", DeviceAttribute.Vendor -> "ProgramCrafter Individual Enterprises", DeviceAttribute.Product -> "PCI-E Byte-to-Byte Visual-Only Decoder" ) override def getDeviceInfo: Map[String, String] = deviceInfo override def tier: Tier = Tier.Two ... И потом напишем её единственный метод.
    ... @Callback(direct = false, doc = """function(data:string):userdata -- Decodes an image in common format (PNG/WebP/JPEG/GIF)""") def decode(context: Context, args: Arguments): Array[AnyRef] = { val data = args.checkByteArray(0) val scrimageImg = ImmutableImage.loader().fromBytes(data) result(new ImageUserdata(scrimageImg)) } } class ImageUserdata(var pixels: ImmutableImage) extends AbstractValue { def this() = this(ImmutableImage.create(8, 8)) // For deserialization @Callback(direct = true, doc = "function():number -- Get image width") def getWidth(context: Context, args: Arguments): Array[AnyRef] = result(pixels.width) @Callback(direct = true, doc = "function():number -- Get image height") def getHeight(context: Context, args: Arguments): Array[AnyRef] = result(pixels.height) @Callback(direct = true, limit = 1000000, doc = "function(x:number, y:number):number -- Get ARGB value at specified coordinates, 1-based") def getARGB(context: Context, args: Arguments): Array[AnyRef] = { val x = args.checkInteger(0) - 1 val y = args.checkInteger(1) - 1 if (x < 0 || x >= pixels.width || y < 0 || y >= pixels.height) throw new IllegalArgumentException("coordinates out of bounds") val p = pixels.pixel(x, y) result(p.alpha(), p.red(), p.green(), p.blue()) } @Callback(direct = false, doc = "function(target_width:number, target_height:number):number -- Get a new image scaled appropriately to fit within specified bounds") def fit(context: Context, args: Arguments): Array[AnyRef] = {...} // Serialization private final val PixelTag = "ScrImage" override def load(nbt: NBTTagCompound, workspace: Workspace): Unit = { super.load(nbt, workspace) pixels = ImmutableImage.loader().fromBytes(nbt.getByteArray(PixelTag)) } override def save(nbt: NBTTagCompound): Unit = { super.save(nbt) nbt.setByteArray(PixelTag, pixels.bytes(PngWriter.MaxCompression)) } }
    Когда всё написанное скомпилируется, обёртка изображения будет поддерживать персистентность, масштабирование, получение размеров и конкретных пикселей картинки.
    Остаётся пока один нерешённый вопрос: а как этот код будет вызываться?

    Кормим Оцелота
     
    На самом деле добавить новую карточку не очень сложно, достаточно:
    нарисовать иконку 16x16, поместить в ocelot-desktop/sprites/items прогнать ocelot-desktop/spritepack и положить новый атлас текстур на место старого добавить новую иконку в Icons.scala (в объекте Items) добавить код ImageCard в репозиторий создать подкласс ImageCardItem with CardItem и соответствующую ему Factory зарегать завод в Items.scala     
    Изменения можно посмотреть на https://gitlab.com/ProgramCrafter/ocelot-desktop/-/compare/develop...ac0acb33.

    Тестируем
     
    Окажется ужасно, если наша карточка не будет работать. Поставим её в компьютер и запустим вот такой скрипт:
    local com = require 'component' local unc = require 'unicode' local img = com.imagine local gpu = com.gpu local function set_halfpixel(x, y, back, fore) local bg, fg = gpu.getBackground(), gpu.getForeground() if bg == fore then gpu.setForeground(back) gpu.set(x, y, unc.char(0x2584)) else if bg ~= back then gpu.setBackground(back) end if fg ~= fore then gpu.setForeground(fore) end gpu.set(x, y, unc.char(0x2580)) end end local filename = (...) or '/home/775251537.0.jpg' local file = io.open(filename, 'rb') local buf = file:read('*a') file:close() local image = assert(img.decode(buf)) local w, h = gpu.getResolution() local image_fit = image.fit(w, (h - 1) * 2) local pts_w, pts_h = image_fit.getWidth(), image_fit.getHeight() io.read() for x = 1, pts_w do for y = 1, pts_h, 2 do local _, r1, g1, b1 = image_fit.getARGB(x, y) local _, r2, g2, b2 = 0,0,0,0 if y + 1 <= pts_h then _,r2,g2,b2 = image_fit.getARGB(x, y + 1) end local top = r1*65536 + g1*256 + b1 local bot = r2*65536 + g2*256 + b2 set_halfpixel(x, (y + 1) // 2, bot, top) end end require'term'.setCursor(1, h) И получим изображение дивана-танка.


    Требуемые улучшения
     
    Проверить, что за библиотека ScrImage, действительно ли она безопаснее или удобнее остальных. (Кажется, что удобнее, ведь она в отличие от javax ImageIO поддерживает WebP.) Сделать так, чтобы открытые картинки потребляли память, потому что сейчас ничто не мешает коду открыть сотню раз по мегабайту. Без этого на сервере такой патч запускать нельзя. Добавить, если не обойдётся вычислительно дорого, фильтры и повороты в интерфейс карточки.  
  2. ProgramCrafter
    После долгого перерыва я наконец смог вернуться к своему моду OCTechnics!
    Решил сначала добавить в мод команду для проверки, всё ли работает правильно.
     
    В этот раз, чтобы узнать, как добавляются свои команды, полинтернета не хватило. Пришлось смотреть уже не гайды, а чистый код OpenComputers.
    Там нашёлся замечательный пример: .../common/command/SimpleCommand.scala.
     
    Сама реализация команды получила отдельный файл: org/octechnics/octechnics/OCTTestCommand.java:
     
    Чтобы команда /oct работала на сервере, нужно дописать ещё несколько строк кода в основной файл OCTechnics.java:
     
    Мод OCTechnics загружается и работает! В этом можно убедиться, вызвав команду /oct: мод ответит "OK, OCTechnics works."
     
    В следующей части уже будет добавление новых блоков, крафтов и блок-сущностей (на самом деле всё это готово, осталось только статью написать).
    Команда пригодится для отладки (я не планирую добавлять к своим блокам GUI, т.е. инвентарь без компьютера просмотреть будет невозможно!)
     
    Для тех, кому интересна разработка мода в реальном времени, рекомендую взглянуть сюда: https://github.com/ProgramCrafter/OCTechnics/.
  3. ProgramCrafter
    Портирование мода с 1.14 на 1.7.10 версию оказалось настоящим эпическим сражением, продлившимся целый день. Сейчас я опишу, как мне удалось в нём победить.
     
    Половину дня я пытался понять, почему один мой Gradle (для Minecraft 1.7.10) не может определить версию Java по строке «12.0.1», а другой (для 1.14) заявляет, что надо указать версию плагина «forge». Ключом к решению оказалось использование OpenJDK 10 и обновление Gradle до 4.1 версии.
     
    На этом проблемы не закончились. Gradle стал скачивать нужные файлы, но не смог скачать их все. Некоторые файлы (такие, как twitch-5.16.jar) отсутствуют на двух сайтах-источниках (minecraftforge.net и ещё один), а с официального сайта Minecraft Java скачать файлы не может, так как там есть проблема с сертификатом. Добавление корневого сертификата с minecraft.net в доверенные (тут как раз пригодилось то, что я скачал дополнительную Java) ничего не дало.
    Я скачал все недостающие файлы через браузер, и тут у меня возникла проблема: а в какую папку их надо положить? Не считать ведь хеш-суммы каждого файла и не раскидывать по отдельным папкам руками?
    Решение оказалось очень простым: если Gradle стучится и проверяет, нет ли файла на files.minecraftforge.net, то этот запрос можно перехватить и направить на локальный сервер, где файл уже есть. Как это можно сделать? Правильно, с помощью файла hosts. Тогда я прописал перенаправление с files.minecraftforge.net на 127.0.0.1, поднял локальный сервер и положил туда все нужные файлы. Запустил Gradle - ура, все файлы скачались! Сервер можно выключать.
     
    Дальше осталось совсем немного: посмотреть в исходники каких-нибудь модов под 1.7.10 и разобраться, как они работают. Поменять в своём коде пришлось (почти) только основной класс. Также изменился файл локализации.
     
    В каком виде мод работает (OCTechnics.java):
     
    Файл ./common/blocks/BasicFactoryBlock.java:
     
    assets/octechnics/lang/en_us.lang:
     
    tile.octechnics:basic_factory_block.name=Factory Base
     
    Текстуры немного переехали: из папки block попали в папку blocks.
     
    Наконец, я запустил мод и он заработал: в первой вкладке творческого инвентаря появился блок с моей текстурой, правильно названный и нормально ставящийся в мир. Этого я и хотел! Дальнейшие приключения (расширение ассортимента и добавление к блокам блок-сущностей) будут в следующей статье.
  4. ProgramCrafter
    Из чего состоит любой завод в Minecraft? Из блоков! Поэтому их надо сделать.
     
    Для удобства сборки я создал .bat скрипт, выполняющий gradlew clean, gradlew build и копирующий мод в папку. Теперь можно поменять что-то в коде (или в другом файле), запустить скрипт и через полминуты уже включать Minecraft, чтобы проверить, всё ли заработало.
     
    В поисках документации, как правильно добавить блок и откуда импортировать пакеты, пришлось перерыть полинтернета. В результате лучшим источником оказались исходники других модов, вроде Forestry или Glassential (первый попавшийся мне под руку мод, добавляющий блоки). Кроме того, я обнаружил странную вещь: все моды используют импорт CreativeTabs, но мой мод с ним отказывается компилироваться, говоря про отсутствие пакета net.minecraft.creativetab.
    Ещё хуже с документацией, как заставить мод реагировать на события. Стандартная шина данных Forge (MinecraftForge.EVENT_BUS) не сработала ни для одного события, которое мне нужно.
     
    Пробовал установить Eclipse для удобства разработки, но он не распознал декомпилированный Minecraft и подсветил все мои импорты красным. Когда я открыл вкладку с задачами Gradle, среда зависла. Тогда я стал работать в Notepad++.
     
    Итоговый вариант, который у меня заработал и стал правильно добавлять блок (файл TestMod.java необходимо переименовать в OCTechnics.java):
    Файл org/octechnics/octechnics/common/blocks/BasicFactoryBlock.java:
    В папке resources: assets/octechnics/blockstates/basic_factory_block.json:
    assets/octechnics/lang/en_us.json:
    assets/octechnics/models/block/basic_factory_block.json:
    assets/octechnics/models/item/basic_factory_block.json:
    Текстуру блока я положил по адресу assets/octechnics/textures/block/factory_base.png.
     
    Следующий этап, который будет описан в отдельной статье - возврат на версию 1.7.10, где есть необходимые моды - IC2 и OpenComputers.
  5. ProgramCrafter
    Благодаря посту «Путь от нуля до разработчика OpenComputers» я вспомнил, что сам тоже хотел сделать моды для Minecraft. Два раза я пытался установить всё необходимое, два раза Gradle ругался на отсутствие JDK.
     
    Почему бы не попробовать ещё раз? К тому же, нашлась замечательная ссылка на документацию Forge: https://mcforge.readthedocs.io/en/latest/gettingstarted/ (спасибо @Totoro).
     
    Итак, я раскопал на диске Gradle от 1.14 версии Minecraft и выполнил новую для себя команду gradlew genEclipseRuns . Жду… нет, ничего не завершается с ошибкой, Minecraft спокойно декомпилируется, и наконец появляется надпись «BUILD SUCCESSFUL». До такого этапа я раньше не доходил, поэтому решил выбрать, какую из своих идей реализовать.
     
    Мне понравилась идея заводика, интегрированного с OpenComputers. Заводик импортирует в себя руду, перерабатывает (с помощью верстаков, печек, дробилок) и экспортирует готовый продукт. При этом каждую команду даёт компьютер из OpenComputers.
     
    Как и положено по документации, я написал в файл TestMod.java:
     
    В файл mcmod.info (прямо внутри папки src/main/resources):
    Пишу gradlew build. Мод собирается. Закидываю его к другим модам, запускаю Minecraft 1.14. Запуск происходит очень спокойно, никакого мода по имени OCTechnics в списке не появилось.
    Что делать? Правильно, искать другие моды под ту же версию (не важно, исходники или скомпилированные моды, главное, чтобы работали). Смотреть, где там лежат файлы и делать такие же файлы у себя.
     
    Нашёлся некий файл pack.mcmeta. Пишем туда (спасибо IronChests):
     
    Кроме того, нужен ещё файл mods.toml:
    На этом этапе я безуспешно пытался понять, какие ссылки (для проверки обновлений мода) сюда надо подключить. В конце концов просто удалил их все. Тогда в логах пропала ошибка MalformedURL, и мод стал опознаваться.
     
    Итоговая схема расположения файлов:
    src - main  - java - org - octechnics - octechnics - TestMod.java
                     - resources - assets - octechnics - lang - en_us.json (пустой)
                                        - META-INF - mods.toml
                                        - mcmod.info
                                        - pack.mcmeta
     
    В следующей записи - создание базового блока для завода.
×
×
  • Создать...