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

Лидеры


Популярный контент

Показан контент с высокой репутацией 14.06.2020 во всех областях

  1. 3 балла
    Первый пост. Ну все же хоть что то. Хочу показать свою небольшую поделку по теме инфраструктуры. Посмотреть и скачать ее можно тут: pastebin run mn7W46KJ Задача программы проста. Вывод значений энергетических ячеек из thermal expansion и заполненность резервуаров из immersive engineering. Автоматическое включение двигатель если средний объем ячеек стал ниже заданного значение и заряд до 100%. Среднее значение по кол во ячейкам подключенным к адаптеру. Делал все через огромные table с id которые потом становятся объеками (эффективно ли это по памяти?) Задача оказалась не так сложна как я думал и больше я завис на графике... Потыкав несколько готовых либ понял что ничего не понял (как это обычно бывает) и решил нарисовать свою графику. Единственное что не понял как запускать event отдельно и реагировать на него поэтому сделал пока вот так: Надеюсь вы подскажите как это лучше решить. Буду благодарен. Ну и скриншотики
  2. 3 балла
    Первый опыт компиляции и правок OpenComputers Задача: Собрать мод OpenComputers, проверить его работоспособность в игре, внести небольшие правки в мод и также проверить их работоспособность в игре. Мой путь к решению: Первая страница поисковой выдачи по фразе «opencomputers build mod» не показала ничего интересного для меня. Зато фраза «opencomputers build from source» быстро привела меня на страницу https://ocdoc.cil.li/tutorial:debug_1.7.10 Команды инструкции несколько отличаются от тех, что я применял раньше. Поэтому я задал себе два вопроса: Чем отличается вызов gradlew от gradle? Чем отличается setupDecompWorkspace от setupCIWorkspace? На первый вопрос я ответил неправильно. Из найденной информации я понял, что обёртка gradlew используется для того, чтобы не морочить себе голову отдельной установкой Gradle и всё необходимое устанавливать через скрипт. Но у меня же уже установлен Gradle! Поэтому проще использовать именно его. Ещё не понимая, в чём грабли, я в хаотическом порядке побежал по коммитам, дойдя чуть ли не до начала репозитория. Но gradle упорно выдавал ошибку даже при запуске без параметров: $ gradle ... A problem occurred evaluating root project 'OpenComputers'. > Failed to apply plugin [id 'forge'] > Could not create task of type 'ReobfTask'. Поиск по фразе «gradle Could not create task of type ReobfTask» не дал ничего вразумительного кроме того, что может быть неправильной версия не то Gradle, не то Forge, не то Minecraft, не то JDK. Так я ходил по граблям около двух часов, пытаясь что-то изменить в конфигах Gradle и переходя от коммита к коммиту. Почувствовав усталость, я решил, что зашёл в тупик, и чтобы выйти из него, мне следует взять перерыв, и отдохнув, найти новую точку для приложения усилий. Так я и сделал. Отдохнув, я ещё раз почитал об отличии gradlew от gradle, вспомнил, что встреченная мной ошибка может быть вызвана неправильной версией Gradle, и сразу осознал упущенный мной нюанс: gradlew – не просто обёртка, и позволяет не просто обойтись без установки gradle, а без установки требуемой версии gradle. Проверяю предположение: $ gradle -version Gradle 2.10 $ ./gradlew -version Gradle 5.6.4 Так и есть! Вывод: Для ускорения продвижения в изучении в первый раз следует максимально чётко следовать инструкциям. А уже имея эталонный рабочий вариант, можно смело экспериментировать. Зная, в чём именно я совершил отклонение, можно быстрее находить и причину неудачи тоже. Я быстро отработал ту часть инструкции, которая не касалась использования IDE, нашёл файл свежесобранного мода, переместил его в каталог с остальными модами и запустил игру: $ git clone https://github.com/MightyPirates/OpenComputers.git $ cd OpenComputers $ ./gradlew setupDecompWorkspace $ ./gradlew build $ find . -name OpenComputers*.jar ./libs/OpenComputers-LuaJ.jar ./libs/OpenComputers-JNLua.jar ./build/libs/OpenComputers-MC1.7.10-1.7.5+f73dd9e-dev.jar ./build/libs/OpenComputers-MC1.7.10-1.7.5+f73dd9e-javadoc.jar ./build/libs/OpenComputers-MC1.7.10-1.7.5+f73dd9e-api.jar ./build/libs/OpenComputers-MC1.7.10-1.7.5+f73dd9e-sources.jar $ mv build/libs/OpenComputers-MC1.7.10-1.7.5+???????-universal.jar ~/.minecraft/mods/OpenComputers-MC1.7.10-1.7.5+test-universal.jar Работает! Отвечая на второй вопрос и вникая в нюансы Gradle, я узнал, что его задачи зависят друг от друга. И если я верно понял, то для сборки мода достаточно лишь скачать репозиторий и запустить сборку мода. Необходимые для этого этапа подзадачи будут выполнены автоматически. Проверяю: Для чистоты эксперимента удаляю папку с модом и пользовательскую папку Gradle: $ rm -rf OpenComputers $ rm -r ~/.gradle И получаю собранный мод минимумом команд: $ git clone https://github.com/MightyPirates/OpenComputers.git $ cd OpenComputers $ ./gradlew build Остаётся лишь перенести мод в каталог с другими модами: $ mv build/libs/OpenComputers-MC1.7.10-1.7.5+???????-universal.jar ~/.minecraft/mods/OpenComputers-MC1.7.10-1.7.5+test-universal.jar С компиляцией и сборкой я разобрался. Теперь пора что-нибудь изменить в моде. Чтобы не выдумывать задачу, я вспоминаю исходную цель. В OpenComputers мне не нравится механика управления нагрузкой от пользовательских скриптов. Что я об этом знаю? Во время длительных вычислений я могу получить ошибку «too long without yielding». Попробую найти эту строку в исходниках: $ grep -ir 'too long without yielding' src/main/resources/assets/opencomputers/lua/machine.lua:local tooLongWithoutYielding = setmetatable({}, { __tostring = function() return "too long without yielding" end}) Удача! Это файл на Lua, и мне сейчас, возможно, не потребуется вникать в Scala. Открываю этот файл первым подвернувшимся под руку редактором: $ nano src/main/resources/assets/opencomputers/lua/machine.lua Ищу, как используется переменная tooLongWithoutYielding. Ошибка с таким исключением генерируется лишь в одном месте, в функции checkDeadline() по результатам проверки computer.realTime() > deadline. Ищу, где и как используется переменная deadline. Стараясь не вникать в детали кода, я нахожу участок, который с наибольшей вероятностью задаёт время, в течение которого пользовательский скрипт может работать без уступки времени: deadline = computer.realTime() + system.timeout(). Лучших вариантов я не вижу, поэтому правлю эту строку. Проверяю выполненные изменения: $ git diff --- a/src/main/resources/assets/opencomputers/lua/machine.lua +++ b/src/main/resources/assets/opencomputers/lua/machine.lua @@ -1486,7 +1486,7 @@ local function main() ... - deadline = computer.realTime() + system.timeout() + deadline = computer.realTime() + 10 --system.timeout() По уже отработанной схеме компилирую мод, переношу его в папку с модами и запускаю игру. Для проверки внесённых в мод изменений я запускаю тестовый скрипт: # lua lua > clock=os.clock t_=clock() pcall(function() while true do end end) t=clock() print(t-t_) 5.000662049 Вроде бы ничего не изменилось. Но я перезагружаю тестовый компик и снова запускаю скрипт. Получаю результат: 9.999750501 Сработало! Подобного поведения можно добиться и банальной правкой конфига, но моя цель заключалась в достижении того же эффекта правкой исходников мода. Результат: Я смог скомпилировать мод OpenComputers, осознал пользу обёртки gradlew, нашёл минимальный набор команд для компиляции, а также внёс работоспособное изменение в мод. Ближайшие планы: Во время решения этой задачи я снова уклонился от использования IDE. Но сейчас я начал серьёзно колебаться, выбирая между двумя направлениями: приступить к поиску оптимального алгоритма управления нагрузкой, или же всё таки освоить работу с IDE хотя бы на базовом уровне.
  3. 2 балла
    Теперь компы работают В РАЗЫ быстрее!!! Вам лишь нужно смазать процессор девятью граммами... Всем привет! Я так долго не заходил на форум, что забыл вас оповестить о новом законном способе ускорить работу компов в игре о котом я узнал. Так делать не нужно!!! Мой комп страдал, чтобы ваш не горевал. Суть вот в чём. В моде ProjectE есть такой замечательный инструмент как часы времени. Ничего особенного они не делают кроме ускорения цикла дня/ночи. НО!!! Если их поставить на пьедестал и активизировать ПКМ, то они судя по описанию немного ускорять животных поблизости и дадут дополнительные 20 тиков блокам в радиусе 3 блоков рядом. (образует куб 7x7x7 с пьедесталом в центре) И тут мы переходим к самому интересному!!! Часы работают на любых блоках: на растениях, блоках из мода и даже на блоках из самого же ProjectE, которые позволяют собирать и накапливать ECM (местную валюту) {из-за чего кстати я сломал аддон к этому моду и быстро прокачался до максимума ECM - просто посмотрите на скрин и всё поймёте} Очень имбовая вещь, ей можно генерировать много пассивной энергии и тратить её в супер быстрый карьер. А эффекты от часов поблизости складываются!!! Так вот, ради чего мы здесь собрались... судя по логике совместимости модов: робот из OpenComputers должен быть блоком, но для часов времени - он не блок и на него ничего не действует. Зато часы действуют на статичные блоки вроде системного блока, монитора и прочего. Что позволяет использовать компы на очень быстрых скоростях. Правда есть и минус. Внутренние часы компа ничего не подозревают и задержки через os.sleep будут так же ускорены как и ВСЯ работа компа. Зато это открывает возможности делать и запускать реальные игры в майнкрафте. Не заботясь о том, что что-то не успеет прогрузится. Вот такие чудеса творятся в мире ProjectE. И его явно никто добавлять на сервера не будет. Зато попробовать программы позапускать с ним можно и в одиночной игре. Может кто-то что-нибудь с этим придумает.
  4. 1 балл
    Среди всех компонентов в OC у интернет-платы самый ужасный API. Неудивительно, что правильно использовать его умеют немногие. Даже за Vexatos мне приходилось чинить tape.lua — программку для записи кассет. Плюс в ирке нередко спрашивают, как отправить HTTP-запрос на сервер. Значит, пришло время написать, как же всё-таки использовать интернет-плату. Гайд строится на следующих предположениях (сорри за педантизм): Вы умеете прогать на Lua, в том числе знаете о двух основных способах возвращать ошибку. Вы писали уже программы для OpenComputers, которые использовали API этого мода или OpenOS, особенно либу event. Вы как-то использовали (или пытались использовать) интернет-карточку в программах. Секции 1, 3: вы понимаете основные принципы HTTP. Секции 2, 4: вы понимаете, как пользоваться TCP-сокетами и зачем (не обязательно в Lua). Секция 4: вас не смущает setmetatable и вы понимаете, как делать ООП на прототипах. Секции 2, 4: у вас OC 1.6.0 или выше. Секции 1, 3, 5: у вас OC 1.7.5 или выше. Текущая версия мода — 1.7.5, а в новой ничего не изменилось. У инет-карты есть две разных фичи — HTTP-запросы и TCP-сокеты. Кратко пробежимся по API и затем разберём детальнее применение. Рассматривать я буду API компонента: часто используют require("internet") — это не компонент, а обёртка. 1. Отправка HTTP-запросов: component.internet.request У этого метода 4 параметра: URL, на который надо послать запрос. На всякий случай, URL начинается со схемы (http: или https:), после которого идёт адрес хоста (например: //localhost, //127.0.0.1, //[::1], //google.com:443), за которым следует путь (/my-file.html). Пример: https://computercraft.ru/blogs/entry/666-profiliruem-programmy-pod-oc/. Данные запроса. Оно же тело запроса. Если мы отправляем GET/HEAD-запрос, то этот аргумент надо установить в nil. Хедеры, которыми запрос сопровождать. Можно поставить nil, тогда там по минимуму дефолтные подтянутся. Иначе передавать надо таблицу. Её ключи — это названия хедеров. Например, {["Content-Type"] = "application/json"}. Метод запроса. Если же этот аргумент не передавать, то возьмётся по дефолту GET или POST: это зависит от того, пуст ли аргумент 2 или нет. Если возникла ошибка, метод вернёт nil и сообщение об ошибке. Если же всё нормально, то метод вернёт handle — табличку с функциями. Вот что это за функции: handle.finishConnect() — проверяет, подключены ли мы к серверу. Если да, то вернёт true. Если к серверу ещё не подключены, то вернёт false. Если же возникла ошибка (например, 404 вернул сервер или закрыл соединение), то вернёт nil и сообщение об ошибке. Например, nil, "connection lost". В доках написано, что функция ошибку пробрасывает. На самом деле нет: она вообще не бросает исключения. handle.response() — возвращает мета-данные ответа с сервера. Если соединение ещё не установлено, вернёт nil. Если возникла ошибка, вернёт nil и сообщение об ошибке. Например, nil, "connection lost". В противном случае возвращает 3 значения: Код ответа (например, 200). Статус (например, "OK"). Таблицу с хедерами, которые отправил сервер. Выглядит примерно так: {["Content-Type"] = {"application/json", n = 1}, ["X-My-Header"] = {"value 1", "value 2", n = 2}}. Выпишу отдельно, что значения таблицы — это не строки, а ещё одни таблицы. handle.read([n: number]) — читает n байт (если n не задано, то сколько сможет). Если компьютер ещё не успел получить данные, то отдаст "". Если возникла ошибка, то выдаст nil и сообщение об ошибке. Например, nil, "connection lost". Если сервер закрыл соединение, то вернёт nil. В противном случае отдаст строку с частью ответа. handle.close() — закрывает соединение. 2. TCP-сокеты: component.internet.connect У метода есть 2 параметра: Адрес хоста. Например, 127.0.0.1. Здесь также можно указать порт: google.com:80. Порт. Если в первом аргументе порта нет, то второй параметр обязателен. Если возникла ошибка, он также вернёт nil и сообщение. Иначе возвращает handle — табличку с функциями. Вот такими: handle.finishConnect() — то же, что и выше. handle.read([n: number]) — то же, что и выше. handle.write(data: string) — отправляет data по сокету на сервер. Возвращает число переданных байт. Если соединение не установлено, это число равно 0. handle.close() — то же, что и выше. handle.id() — возвращает id сокета. 3. Как правильно отправить HTTP-запрос на сервер и получить ответ Чтобы было интереснее, реальная задача: написать аналог pastebin, только вместо пастбина использовать https://clbin.com/. Особенности: Для взаимодействия с сайтом нужно отправлять HTTP-запросы: GET и POST. Это всё OC умеет. Чтобы скачать, достаточно простого GET по ссылке. Это можно сделать даже через wget. А вот чтобы отправить файл, надо использовать MIME-тип multipart/form-data. OC не умеет из коробки такие формы отправлять. Мы напишем минимальную реализацию, которая бы нас устроила. Не забываем, что этот MIME-тип нужно установить в хедер. При этом мы хотим красиво обработать все ошибки и не допустить ошибок сами. Таким образом, использовать будем практически все фичи. 3.1. multipart/form-data Порядок особенностей нам не важен, поэтому начинаем с самого скучного. Сделаем функцию, которая принимает данные и обрамляет их согласно формату multipart/form-data. local function generateBorder(str) local longestOccurence = nil for match in str:gmatch("%-*cldata") do if not longestOccurence or #match > #longestOccurence then longestOccurence = match end end return longestOccurence and ("-" .. longestOccurence) or "cldata" end local function asFormData(str, fieldName) local border = generateBorder(str) local contentType = "multipart/form-data; boundary=" .. border return ([[ --%s Content-Disposition: form-data; name="%s" %s --%s--]]):format( border, fieldName, str, border ), contentType end Так как это не туториал по интернет-стандартам, вдаваться в детали реализации не буду. С помощью asFormData можно содержимое файла превратить в тело HTTP-запроса. Мы будем вызывать asFormData(str, "clbin"), ибо этого требует сайт. Кроме того, эта функция нам передаст значение хедера Content-Type. Он нам понадобится. 3.2. Взаимодействие с сайтом Напишем теперь функцию — обёртку над component.internet.request. local function request(url, body, headers, timeout) local handle, err = inet.request(url, body, headers) -- ① if not handle then return nil, ("request failed: %s"):format(err or "unknown error") end local start = comp.uptime() -- ② while true do local status, err = handle.finishConnect() -- ③ if status then -- ④ break end if status == nil then -- ⑤ return nil, ("request failed: %s"):format(err or "unknown error") end if comp.uptime() >= start + timeout then -- ⑥ handle.close() return nil, "request failed: connection timed out" end os.sleep(0.05) -- ⑦ end return handle -- ⑧ end Эту функцию можно прямо брать и копипастить в свои программы. Что она делает: ① — отправляем запрос. Сразу обрабатываем ошибку. ② — запрос доходит до сервера не мгновенно. Нужно подождать. Чтобы не зависнуть слишком долго, мы засекаем время начала. ③ — вызываем finishConnect, чтобы узнать статус подключения. ④ — finishConnect вернул true. Значит, соединение установлено. Уходим из цикла. ⑤ — finishConnect вернул nil. Мы специально проверяем через status == nil, потому что не нужно путать его с false. nil — это ошибка. Поэтому оформляем его как ошибку. ⑥ — проверяем, висим ли в цикле мы слишком долго. Если да, то тоже возвращаем ошибку. Не забываем закрыть за собой соединение. ⑦ — нам не нужен бизи-луп. Спим. ⑧ — мы не читаем сразу всё в память, чтобы экономить память. Вместо этого отдаём наружу handle. Частая ошибка — отсутствие элементов ②–⑦. Они нужны. Если до установки соединения мы вызовем handle.read(), то получим nil. Многие программы в этом случае сразу отчаются получить ответ и вернут ошибку. А надо было просто подождать. 3.3. Отправка файла Функция для отправки файла должна сначала прочесть его содержимое, затем сделать запрос и прочесть ответ. В ответе будет находиться URL файла. local function sendFile(path) local f, err = io.open(path, "r") -- ① if not f then return nil, ("could not open file for reading: %s"):format(err or "unknown error") end local contents = f:read("*a") -- ② f:close() local data, contentType = asFormData(contents, "clbin") -- ③ local headers = {["Content-Type"] = contentType} local handle, err = request("https://clbin.com", data, headers, 10) -- ④ if not handle then return nil, err end local url = {} -- ⑤ local read = 0 local _, _, responseHeaders = handle.response() -- ⑥ local length for k, v in pairs(responseHeaders) do -- ⑦ if k:lower() == "content-length" then length = tonumber(v) end end while not length or read < length do -- ⑧ local chunk, err = handle.read() if not chunk then if length then -- ⑨ return nil, ("error occured while reading response: %s"):format(err or "unknown error") -- ⑩ end break -- ⑩ end read = read + #chunk -- ⑪ if length and read > length then chunk = chunk:sub(1, length - read - 1) -- ⑫ end table.insert(url, chunk) end handle.close() -- ⑬ return table.concat(url) -- ⑭ end ① — открываем файл для чтения. Обрабатываем ошибки. ② — считываем всё из файла. Не забываем закрыть его за собой. ③ — вызываем заранее написанную функцию asFormData. Мы получаем тело запроса и значение хедера Content-Type. Создаём таблицу хедеров. ④ — отправляем наш запрос. Обрабатываем ошибки. ⑤ — handle.read может не сразу вернуть весь ответ, а кусочками. Чтобы не забивать память кучей строк, кусочки мы будем класть в таблицу (получится что-то вроде {"htt", "p://", "clbi", "n.co", "m/ab", "cdef"}). Также мы храним число прочитанных байт. ⑥ — мы хотим сверять число прочитанных байт с ожидаемым размером ответа. Для этого нам потребуется получить хедеры, отправленными сервером. Вызываем handle.response. ⑦ — размер ответа обычно пишется в заголовок Content-Length. Однако сервер может поиграться с регистром. Например, писать content-length или CONTENT-LENGTH. OpenComputers не трогает эти хедеры. Поэтому придётся пройтись по всем ключам таблицы и найти хедер без учёта регистра. ⑧ — если length не nil, то это число. Тогда проверяем, что ещё столько байт мы не прочли, и заходим в цикл. Если же Content-Length не задан, то будем считать, что серверу не важно, сколько надо прочесть, и крутимся до упора. ⑨ — handle.read может ещё вернуть ошибку. Если нам известна длина, то в силу условия цикла мы прочли меньше, чем ожидали. Сигналим о неудаче. (Закрывать соединение в случае ошибки не требуется.) ⑩ — если же длина неизвестна, то считаем, что сервер отдал всё, что мог, ошибку игнорируем и покидаем цикл. ⑪ — не забываем обновлять read. ⑫ — если сервер случайно отослал нам больше данных, чем надо (а мы знаем, сколько надо: length определён), то излишки обрезаем. Код здесь отрежет с конца строки (read - length) байт. ⑬ — закрываем соединение за собой, когда оно больше не нужно. ⑭ — наконец, склеиваем таблицу в одну строку. 3.4. Скачивание файлов Код для скачивания похож на предыдущий. Только вот в память мы записывать ответ с сервера уже не будем. Вместо этого напрямую пишем в файл. local function getFile(url, path) local f, err = io.open(path, "w") -- ① if not f then return nil, ("could not open file for writing: %s"):format(err or "unknown error") end local handle, err = request(url, nil, nil, 10) -- ② if not handle then return nil, err end local read = 0 local _, _, responseHeaders = handle.response() local length for k, v in pairs(responseHeaders) do if k:lower() == "content-length" then length = tonumber(v) end end while not length or read < length do local chunk, err = handle.read() if not chunk then if length then f:close() -- ③ return nil, ("error occured while reading response: %s"):format(err or "unknown error") end break end read = read + #chunk if length and read > length then chunk = chunk:sub(1, length - read - 1) end f:write(chunk) end f:close() -- ④ handle.close() return true end ① — открываем файл, в этот раз для записи. Обрабатываем ошибки. ② — отправляем запрос без данных и с дефолтными хедерами. Обрабатываем ошибки. ③ — если мы сюда попали, то дальше сделаем ретурн. Поэтому не забываем закрывать за собой файл. (Сокет закрывать не нужно, так как при ошибке он это делает сам.) ④ — добропорядочно освобождаем ресурсы. Чтобы было удобнее копипастить, я оставил повторяющийся код в двух функциях. В своей программке можно sendFIle и getFile отрефакторить, выделить дублирующуюся часть в отдельную функцию. 3.5. UI Пришло время красивой каденции. Аккордом финальным в ней будет пользовательский интерфейс. Он к интернет-карте отношения уже не имеет, но для полноты приведу и его. local args, opts = shell.parse(...) local function printHelp() io.stderr:write([[ Usage: clbin { get [-f] <code> <path> | put <path> } clbin get [-f] <code> <path> Download a file from clbin to <path>. If the target file exists, -f overwrites it. clbin put <path> Upload a file to clbin. ]]) os.exit(1) end if args[1] == "get" then if #args < 3 then printHelp() end local code = args[2] local path = args[3] local url = ("https://clbin.com/%s"):format(code) path = fs.concat(shell.getWorkingDirectory(), path) if not (opts.f or opts.force) and fs.exists(path) then io.stderr:write("file already exists, pass -f to overwrite\n") os.exit(2) end local status, err = getFile(url, path) if status then print("Success! The file is written to " .. path) os.exit(0) else io.stderr:write(err .. "\n") os.exit(3) end elseif args[1] == "put" then if #args < 2 then printHelp() end local path = args[2] local url, err = sendFile(path) if url then url = url:gsub("[\r\n]", "") print("Success! The file is posted to " .. url) os.exit(0) else io.stderr:write(err .. "\n") os.exit(4) end else printHelp() end 3.6. Вуаля Осталось добавить реквайры, и мы получим полноценный клиент clbin. Результат — на гисте. 4. Как правильно установить соединение через TCP-сокет Прошлая секция была вроде интересной, поэтому здесь тоже запилим какую-нибудь программку. @Totoro вот сделал интернет-мост Stem. Напишем для него клиент. Правильно. Опять же, особенности: Работает через TCP-сокет. Протокол бинарный. И асинхронный. А ещё сессионный: у каждого TCP-соединения есть собственный стейт. Доки хранятся на вики. При разрыве соединения клиент должен переподключиться и восстановить стейт. Здесь снова придётся использовать все фичи интернет-карты. 4.1. Архитектура Мы разделим программу на 2 части — фронтенд и бэкенд. Фронт будет заниматься рисованием и приёмом данных от пользователя, и им займёмся в конце и без комментариев. Бэк — поддержанием соединения и коммуникации с сервером. Это куда больше имеет отношения к гайду, рассмотрим подробнее. Бэкенд реализуем через ООП. Создадим конструктор, напихаем методов, которые затем будет дёргать фронт. 4.2. Конструктор Привычно вбиваем ООП-шаблон в Lua. local newClient do local meta = { __index = {}, } function newClient(address, channels, connectionTimeout, readTimeout, maxReconnects) local obj = { __address = address, __channels = channels, __connectionTimeout = connectionTimeout, __readTimeout = readTimeout, __maxReconnects = maxReconnects; __socket = nil, __buffer = nil, __running = false, __reconnectCount = 0, } return setmetatable(obj, meta) end end Ну, тут всё мирно пока. Начнём боевые действия с протокола. 4.3. Протокол Для него наклепаем кучу методов, которые будут крафтить пакеты и писать их через write. Write сделаем позже. Также сразу сделаем персеры. local meta = { __index = { __opcodes = { message = 0, subscribe = 1, unsubscribe = 2, ping = 3, pong = 4, }, __craftPacket = function(self, opcode, data) return (">s2"):pack(string.char(opcode) .. data) end, __parsePacket = function(self, packet) local opcode, data = (">I1"):unpack(packet), packet:sub(2) return self.__parsers[opcode](data) end, send = function(self, channel, message) return self:write(self:__craftPacket(self.__opcodes.message, (">s1"):pack(channel) .. message)) end, subscribe = function(self, channel) return self:write(self:__craftPacket(self.__opcodes.subscribe, (">s1"):pack(channel))) end, unsubscribe = function(self, channel) return self:write(self:__craftPacket(self.__opcodes.unsubscribe, (">s1"):pack(channel))) end, ping = function(self, message) return self:write(self:__craftPacket(self.__opcodes.ping, message)) end, pong = function(self, message) return self:write(self:__craftPacket(self.__opcodes.pong, message)) end, }, } meta.__index.__parsers = { [meta.__index.__opcodes.message] = function(data) local channel, idx = (">s1"):unpack(data) return { type = "message", channel = channel, message = data:sub(idx), } end, [meta.__index.__opcodes.subscribe] = function(data) return { type = "subscribe", channel = (">s1"):unpack(data), } end, [meta.__index.__opcodes.unsubscribe] = function(data) return { type = "unsubscribe", channel = (">s1"):unpack(data), } end, [meta.__index.__opcodes.ping] = function(data) return { type = "ping", message = data, } end, [meta.__index.__opcodes.pong] = function(data) return { type = "pong", message = data, } end, } В коде я активно использую string.pack и string.unpack. Эти функции доступны только на Lua 5.3 и выше, но позволяют очень удобно работать с бинарными форматами. 4.4. Подключение к серверу Прежде чем реализуем write, нужно разобраться с подключением. Оно нетривиально. local meta = { __index = { ..., connect = function(self) local socketStream = assert(inet.socket(self.__address)) -- ① local socket = socketStream.socket -- ② local start = comp.uptime() -- ③ while true do local status, err = socket.finishConnect() if status then break end if status == nil then error(("connection failed: %s"):format(err or "unknown error")) -- ④ end if comp.uptime() >= start + self.__connectionTimeout then socket.close() error("connection failed: timed out") -- ④ end os.sleep(0.05) end self.__socket = socket -- ⑤ self.__buffer = buffer.new("rwb", socketStream) -- ⑥ self.__buffer:setTimeout(self.__readTimeout) -- ⑦ self.__buffer:setvbuf("no", 512) -- ⑧ for _, channel in ipairs(self.__channels) do -- ⑨ self:subscribe(channel) end end, }, } ① — я использую обёртку над component.internet. Она потом будет нужна, чтобы мы могли поместить сокет в буфер. Обращаю внимание, что вызов обёрнут в assert. Работает она так: если первое значение не nil и не false, то возвращает его, а иначе кидает ошибку, используя второе значение в качестве сообщения. Проще говоря, она превращает nil, "error message" в исключение. ② — а пока я вытягиваю из обёртки сокет... ③ — чтобы можно было проверить, установлено ли соединение. Код здесь аналогичен тому, что мы делали в прошлой секции. Не выдумываем. ④ — одно различие: вместо return nil, "error message" я сразу прокидываю исключение. Прежде всего потому, что ошибки мы прокидывать должны единообразно. Раз в ① кидаем исключение, и здесь делаем то же. Почему исключение, а не return nil, "error message"? Мы вызывать connect будем из всяких мест. Так как в случае ошибок бэкенд беспомощен, то лучше прокинуть ошибку до фронтенда и не усложнять код бэка проверками на nil. Кроме того, это громкая ошибка: если забыть где-то её обработать, она запринтится на экран, случайно пропустить её или подменить какой-нибудь непонятной "attempt to index a nil value" не получится. В конце концов, мне так проще. ⑤ — сокет я сохраняю в поле. socket.finishConnect нам ещё понадобится. ⑥ — пришло время обернуть сокет в буфер. Может показаться излишним, особенно учитывая ⑧. Причины станут ясны, когда будем делать чтение. rw — это буфер для чтения и записи. b — бинарный режим: buffer:read(2) вернёт 2 байта, а не 2 символа. Так как символы кодируются в UTF-8 и занимают 1 (латиница), 2 (кириллица, диакритика), 3 (BMP: куча письменностей, всякие графические символы, большая часть китайско-японско-корейских иероглифов) или 4 байта (всё, что не влезло в BMP, например emoji), то отсутствие этого режима может дать ощутимую разницу. В нашем случае протокол бинарный — ставим b. ⑦ — устанавливаем таймаут для чтения. Объясню подробнее, когда будем это чтение делать. ⑧ — отключаем буфер для записи. Он нам не нужен. ⑨ — здесь же подключаемся ко всем каналам. Итого мы получаем свойства __socket и __buffer. Сокет использовать будем, чтобы вызывать .finishConnect() и .id(). Буфер — для записи и чтения. 4.5. Запись Теперь, разобравшись с сокетами и буферами, мы можем запросто писать в сокет. Пилим write: local meta = { __index = { ..., write = function(self, data) return assert(self.__buffer:write(data)) end, }, } Здесь тоже оборачиваем write в assert, чтобы кидать исключения. Причины уже пояснял. 4.6. Чтение и обработка пакета Сначала делаем функцию readOne. Она будет пытаться читать ровно один пакет. Здесь требуется нестандартная обработка ошибок, поэтому код сложноват. local meta = { __index = { ..., readOne = function(self, callback) -- ⑥ self.__buffer:setTimeout(0) -- ① local status, head, err = pcall(self.__buffer.read, self.__buffer, 2) self.__buffer:setTimeout(self.__readTimeout) if not status and head:match("timeout$") then return end assert(status, head) -- ② local length = (">I2"):unpack(assert(head, err)) -- ③ local packet = self:__parsePacket(assert(self.__buffer:read(length))) -- ④ if packet.type == "ping" then -- ⑤ self:pong(packet.message) end callback(self, packet) -- ⑥ return true end, } } ① — рассмотрим эту мишуру по порядку: Любой пакет stem начинается с 2 байт, которыми кодируется длина остатка. Отсюда всплывает двойка. Автор buffer, к сожалению, не осилил реализовать адекватную обработку ошибок. Он использует и исключения, и тихие ошибки (nil, "error message"). В случае таймаута будет прокинуто исключение. Однако мы перед чтением поставили таймаут в 0. Если буфер не найдёт сразу 2 байта в сокете, то он сразу кинет ошибку. Мы хотим проверить, есть ли в сокете пакет, который бы можно было прочесть. Используем pcall. Сначала раскроем self.__buffer:read(2) как self.__buffer.read(self.__buffer, 2), а затем поместим функцию и её аргументы в pcall. pcall возвращать будет сразу 3 значения по следующему принципу: Если на сокете есть 2 непрочитанных байта, read вернёт их без ошибок. Тогда status будет равен true, в head сохранятся эти 2 байта, а в err запишется nil. Если на сокете этих байтов нет, то read прокинет исключение "timeout". status установится в false, head приравняется "/lib/buffer.lua:74: timeout", а err также будет nil. Если же при чтении с сокета возникла другая ошибка, то read вернёт её по-тихому: status будет true, head — nil, а сообщение об ошибке уйдёт в err. Не думаю, что этот случай возможен, однако read может кинуть исключение и не из-за таймаута. status установится в false, а ошибка сохранится в head. В if мы проверяем, был ли таймаут (ситуация 1.2). В таком случае мы не кидаем исключения, а тихо выходим. Наконец, не забываем вернуть прежнее значение таймаута. ② — обрабатываем случай 1.4. ③ — обрабатываем случай 1.3 с помощью assert. Последний оставшийся и единственный успешный случай (1.1) также покрывается: распаковываем 2 байта в целое беззнаковое число (uint16_t). ④ — в ③ мы получили длину оставшегося пакета. Очевидно, надо остаток дочитать, что и делаем. Здесь уже не надо отдельно обрабатывать таймаут, достаточно assert. Считанный пакет отдаём в __parsePacket. ⑤ — если сервер докопался до нас своим пингом, отправим ему понгу. ⑥ — функция readOne принимает коллбэк. Это функция, которая будет обрабатывать все пакеты. Коллбэк будет передавать фронтенд, а бэкенд займётся минимальной обработкой, чтобы в принципе работало. Как, например, ③. Отлично. Мы приготовили все примитивы, которые были нужны. Осталось собрать их воедино — в event loop. 4.7. Event loop и события Ивент луп — это цикл, который ждёт событий и что-то с ними делает. Пришло время разобраться, что за события есть в OC. Когда мы вызываем socket.read или socket.finishConnect, устанавливается "ловушка" (селектор). Она срабатывает, когда на сокет пришли новые байты. При этом компьютер получает событие internet_ready. После чего "ловушка" деактивируется до следующего вызова. internet_ready, таким образом, — это событие, извещающее нас о том, что на сокете валяются непрочитанные данные и пора вызвать socket.read, чтобы их собрать. У события два параметра. Первый — это адрес интернет-карты. Второй — id сокета. Тот id, который возвращает socket.id(). Поэтому мы сохранили сокет в поле __socket: сейчас будем использовать его. local meta = { __index = { ..., __run = function(self, callback) while self.__running do local e, _, id = event.pullMultiple(self.__readTimeout, "internet_ready", "stem%-client::stop") -- ① if e == "internet_ready" and id == self.__socket.id() then -- ② while self:readOne(callback) do self.__reconnectCount = 0 -- ③ end elseif e ~= "stem-client::stop" then self:ensureConnected() -- ④ end end end, stop = function(self) self.__running = false event.push("stem-client::stop") -- ⑤ end, } } ① — ждём события internet_ready или stem-client::stop. Так как в event.pullMultiple названия ивентов сверяются через string.match, дефис экранируем. Второй ивент нужен, чтобы принудительно прервать цикл из stop. ② — обрабатываем мы только internet_ready и только для нашего сокета. Проверяем. ③ — если поймался пакет или пакеты, то пытаемся обработать каждый в порядке прибытия. Когда мы закончили обрабатывать все пакеты, self:readOne вернёт nil, и цикл прервётся. Кстати говоря, если мы внутри цикла оказались, то соединение установилось. Не забываем отметить это. ④ — если же улов пуст, перепроверяем, подключены ли мы вообще. ⑤ — не забываем добавить метод, чтобы остановить наш цикл. Отсюда же отсылаем событие stem-client::stop. Отлично. Теперь пришло время ловить все наши прокидываемые исключения. 4.8. Обработка ошибок Последними 2 функциями, которые мы добавим, будут ensureConnected и run. С их помощью бэкенд будет автоматически переподключаться к серверу в случае проблем. local meta = { __index = { ..., ensureConnected = function(self) local status, err = self.__socket.finishConnect() -- ① if status == false then error("not yet connected") end return assert(status, err or "unknown error") end, run = function(self, callback) if self.__running then -- ② return end self:connect() -- ③ self.__running = true while self.__running do -- ④ local status, err = pcall(self.__run, self, callback) -- ⑤ if not status then if self.__reconnectCount == self.__maxReconnects then -- ⑥ return nil, ("connection lost: %s; reconnect limit is reached"):format(err or "unknown error") end self.__reconnectCount = self.__reconnectCount + 1 self.__buffer:close() -- ⑦ if not pcall(self.connect, self) then -- ⑧ if self.__socket then self.__socket:close() end if self.__buffer then self.__buffer:close() end os.sleep(1) end end end self.__buffer:close() end, }, } ① — ensureConnected просто прокинет ошибку, которую вернёт finishConnect(). ② — принимаем защитную позицию против дураков. Рекурсивно запускать циклы смысла нет. ③ — сначала подключаемся к серверу. Если всё отлично, то можно начинать. ④ — как и в __run, здесь мы оборачиваем код в цикл. Если вызван stop(), то сначала остановится self.__run, а затем и этот цикл. ⑤ — обработка исключений требует pcall. Потому что их надо словить. ⑥ — если мы старались-старались, но так и не смогли уложиться в self.__maxReconnects по реконнектам, кидаемся белым флагом. ⑦ — не забудем закрыть буфер. ⑧ — вспомним, что self.connect кидает исключение. Перехватываем. На всякий случае позакрываем то, что породил connect. 4.9. Фронтенд На этом наш бэкенд готов. Поздравляю. Остаётся лишь прицепить ввод-вывод. Опять же, даю готовый код без комментариев, ибо не об этом пост. local gpu = com.gpu local w, h = gpu.getResolution() local function writeLine(color, line) local oldFg if gpu.getForeground() ~= color then oldFg = gpu.setForeground(color) end local lines = 0 for line in text.wrappedLines(line, w + 1, w + 1) do lines = lines + 1 end gpu.copy(1, 1, w, h - 1, 0, -lines) local i = 0 for line in text.wrappedLines(line, w + 1, w + 1) do gpu.set(1, h - lines + i, (" "):rep(w)) gpu.set(1, h - lines + i, line) i = i + 1 end if oldFg then gpu.setForeground(oldFg) end end local channel = ... if not channel then io.stderr:write("Usage: stem <channel>\n") os.exit(1) end if #channel == 0 or #channel >= 256 then io.stderr:write("Invalid channel name\n") os.exit(2) end local client = newClient( "stem.fomalhaut.me:5733", {channel}, 10, 10, 5 ) require("thread").create(function() while true do term.setCursor(1, h) io.write("← ") local line = io.read() if not line then break end local status, err = pcall(client.send, client, channel, line) if not status then writeLine(0xff0000, ("Got error while sending: %s"):format(err or "unknown error")) break end end client:stop() end) client:run(function(client, evt) if evt.type == "message" then writeLine(0x66ff00, "→ " .. evt.message) elseif evt.type == "ping" or evt.type == "pong" then writeLine(0xa5a5a5, "Ping: " .. evt.message:gsub(".", function(c) return ("%02x"):format(c:byte()) end)) end end) os.exit(0) Здесь я упускаю одну вещь: обработку ошибок в client.send. Если мы попытаемся отправить сообщение, когда у нас потеряно соединение (или до того, как оно установлено), мы или словим ошибку, или потеряем сообщение. Починить это можно, добавив очередь отправляемых пакетов, но это в разы усложнит программу, поэтому оставим так. 4.10. Готово! Добавим реквайров... И у нас получился вполне рабочий клиент для Stem! Код программы — на гисте. 5. В чём различие между component.internet и require("internet") Первое — исходный компонент. Второе — обёртка над ним. У обёртки есть 3 функции: internet.request(url, data, headers, method) — обёртка над component.internet.request. Удобна тем, что все ошибки превращает в исключения за программиста. Кроме того, возвращаемое значение — итератор, и его можно поместить в цикл for. Тем не менее, код, который ждёт установки соединения, нужно писать самому. internet.socket(address, port) — промежуточная обёртка над component.internet.connect. Она используется для того, чтобы потом превратить её в буфер, как сделали мы. Сама по себе достаточно бесполезна. internet.open(address, port) — тоже обёртка над component.internet.connect. Она вызывает internet.socket(address, port) и сразу превращает результат в буфер. Проблема в том, что сам объект сокета использовать можно только через приватные свойства, которые могут ломаться между обновлениями OpenOS. Из-за этого функция исключительно ущербна. Для отправки HTTP-запросов я предпочитаю использовать API компонента. TCP-сокеты же проще создавать через обёртку (internet.socket), вручную проверять подключение и так же вручную укладывать обёртку в буфер, как показано выше. 6. Конец Самое сложное в использовании интернет-карты — это правильно обработать все ошибки. Они могут возникнуть на каждом шагу, при этом быть полноценными исключениями или тихими ошибками. Необработанные исключения крашат программу, из-за чего возникает желание весь код программы поместить в один большой pcall. Например, IRC-клиент, который на дискете поставляется, делает так. Тихие ошибки гораздо подлее. Необработанные, они тоже крашат программу, только вот сама ошибка теряется, подменяется другой (обычно "attempt to index a nil value"). В Lua обработать все ошибки — задача сложная, потому что механизм ошибок ужасен. В нормальных языках стэктрейс отделён от сообщения об ошибке, плюс каждая ошибка имеет свой тип, по которому можно безопасно определять вид ошибки. Lua этим не заморачивается: сообщение об ошибке включает позицию в коде, откуда ошибка прокинута. Есть или нет стэктрейс, зависит от выбора между pcall и xpcall. Если они находятся где-то в другой библиотеке, программист на выбор повлиять не может. В коде Stem-клиента единственный способ узнать, от таймаута ли ошибка прокинута, — матчить последние 7 символов на слово "timeout". Это эталонный костыль. Даже в JavaScript механизм лучше. Поэтому этот пост получился не столько про интернет-карту, сколько про обработку ошибок.
  5. 1 балл
    GradleWrapper предназначен для того, чтобы сделать воркспейсы более переносимыми. Не факт, что твоя глобальная версия Gradle правильно все сделает по билд-скрипту. С другой стороны враппер позволяет всем разработчикам проекта иметь идентичный воркспейс, если заработало(или не заработало) у одного, то аналогично будет у всех. Т.к. скорее всего такой алгоритм будет зависеть от аспектов игры(типо, тпс, отгрузка чанков), то скорее всего понадобится вносить изменения в код самого мома, смотреть исходники игры. Поэтому будет хорошей идей попробовать работать в идее(), она довольно хорошо заточена под скалу
  6. 1 балл
    Много существует этих способов. Например, для экспериментов в креативе я использую TickrateChanger. Очень помогает ускорить отладку программ, работающих с объектами мира. Но есть нюансы. Для достижения высокого TPS требуется иметь быстрый процессор. Если реальный комп не справляется с нагрузкой, то часть тиков пропускается, причём, в неуправляемом порядке. Недавно разработчики OpenComputers слегка модифицировали алгоритм работы GPU. Это тоже позволяет сильно увеличить FPS на экранах без необходимости поднимать TPS. Можешь посмотреть в экспериментальных сборках OpenComputers.
  7. 1 балл
    Добрый день, надеюсь не забыли обо мне! А я пришёл вам новую программу показать Давайте разбираться... Пример работы: симуляция магнитного поля между двумя полюсами Описание: Программа написана на языке программирования lua и работает на движке love2d Визуализация использует GLSL шейдеры версии 3 Для хранения работ используются отдельные файлы с шейдерами, чтобы не трогать весь код программы Кнопки внизу экрана скрываются до наведения на них мышкой Кнопки хранятся как объект со своими функциями и свойствами внутри. И вкладываются в массив инструментария Пример работы: волны искажений Возможности: Навигация и/или зуммирование на колесо мыши Несколько типов анимации. (на кнопку включается анимация движения волн, остальные типы пишутся в шейдерах) Сохранение кадра Сохранение анимации Настройка "шага волны" и скорости течения времени кнопками (обязательно необходимо для построения кадра) Видеообзор: Готовые gif анимации: Рисунок электромагнитных полей: Волновые искажения: Цветные иллюзии: Иллюзия в том, что кружков (кроме двойного) нет. На этих участках случайно совпадают цвета Прочее: Обычные рисунки: Эксперименты над волнами: История версий: Кто-то тут может сказать: "а где ссылка на программу?" А её и не будет пока не реализую нормальную систему сохранений. Почему тогда я выкладываю недоработку? Потому что мне нужно рассказать о проекте для резюме. Это моя лучшая программа и я её люблю. Оттягивать нет смысла, первая версия разрабатывалась ещё полтора года назад. А значит до релиза четвёртой ждать ещё неизвестно сколько. Так что спасибо за внимание! Если вдруг кому-то тема визуализации так же интересна как и мне - пишите в ЛС. Было бы интересно обсудить.
  8. 1 балл
    Совсем забыл в видео показать. Демонстрация подключения компонентов "на лету":
  9. 1 балл
    Небольшое обновление: Добавлена поддержка локализаций. Пока что не вынесено в отдельный файл - возможно вынесу. Сменить язык можно изменив stuff.language на EN - английский, RU - русский, соответственно. Убраны часы, добавлен показатель расхода оперативной памяти. (Стырено с майнос) Добавлен "компас". Позволяет проще ориентироваться, в какую сторону смотрит дрон. И самая здоровская фича которая была добавлена - автодополнение. Теперь, интерпретатор вообще ничем не отличается от того, что есть в стандартной поставке OpenOS. Огромное спасибо @hohserg. Небольшая гифка:
  10. 1 балл
    Наткнулся на плеер NBS музыки через computronic's. Тема на англоязычном форуме: https://oc.cil.li/topic/1758-noteblock-studio-player-for-computronics-sound-cards/ Что получилось у меня: Получилось довольно забавно, учитывая что дроном можно управлять во время проигрывания музыки, то есть теперь на свинолёте можно лететь не в тишину, а с музычкой =) Порядок действий, как проигрывать музыку: Обновить программу Скачать какой-нибудь .nbs файл Теперь, через net можно сделать так: net <имя-файла>.nbs <имя> nbs файл сохранился как переменная, теперь создаём какой-нибудь файл, например play.lua, редактируем - пишем туда nbsPlay(nbs: string, repeat: boolean). В моём случае - nbsPlay(despacito, true) (Будет постоянный повтор музыки, остановить можно написав error() в интерпретаторе) Пишем net play.lua Слушаем и радуемся! !ВНИМАНИЕ! Требовательно к оперативной памяти. Минимальная конфигурация с т 1.5 плашкой первого уровня, иначе дрон будет кричать о недостаточной памяти. Так же, с 1 платой при требовательных операциях и работающей музыке - возможен вылет дрона.
Эта таблица лидеров рассчитана в Москва/GMT+03:00
×
×
  • Создать...