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

Поиск по сайту

Результаты поиска по тегам 'Lua'.

  • Поиск по тегам

    Введите теги через запятую.
  • Поиск по автору

Тип публикаций


Блоги

  • Робот Байт
  • Fingercomp's Playground
  • 1Ridav' - блог
  • Totoro Cookies
  • Блог cyber01
  • IncluderWorld
  • KelLiN' - блог
  • Крутой блог
  • eutomatic blog
  • Programist135 Soft
  • Сайт в сети OpenNet
  • PieLand
  • Очумелые ручки
  • Блог недоблоггера
  • В мире Майнкрафт
  • LaineBlog
  • Квантовый блог
  • Блог qwertyMAN'а
  • some blog name
  • Дача Игоря
  • Путешествия Xytabich'а
  • Рецепты программирования

Форумы

  • Программирование
    • Программы
    • База знаний
    • Разработчикам
    • Вопросы
  • Игровой раздел
    • Игровые серверы
    • Моды и плагины
    • Жалобы
    • Ивенты и конкурсы
    • Файлы
  • Общение
    • Задать вопрос
    • Обратная связь
    • Беседка
    • Шкатулка
  • Технический раздел
    • Корзина

Группы продуктов

Нет результатов для отображения.


Искать результаты в...

Искать результаты, которые...


Дата создания

  • Начать

    Конец


Последнее обновление

  • Начать

    Конец


Фильтр по количеству...

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

  • Начать

    Конец


Группа


AIM


MSN


Сайт


ICQ


Yahoo


Jabber


Skype


ВКонтакте


Gtalk


Facebook


Twitter


Город


Интересы

Найдено 153 результата

  1. Многие, кто играл в майн с древних времен, помнят, что был такой замечательный мод RedPower2. Помимо всяких крутых механизмов там были компьютеры, работающие на forth-системе. Функционал, правда не богатый, можно было только мигать цветными кабелями. Мод развития не получил, автор пропал. Потом был мод NedoComputers, но он тоже не долго прожил и особого распространения не получил. Есть идея, написать виртуальную машину для OpenComputers. Язык Forth невероятно примитивен, синтаксис простой и лаконичный, базовая система легко уместится на EEPROM. Но есть пара вопросов в реализации. Так как придется писать интерпретатор/компилятор на языке высокого уровня, надо чем-то пожертвовать или отойти от стандарта. Язык плотно работает со стеком. Есть стек данных и стек возвратов (второй пока не трогаем). Адресация 16 бит, следовательно, диапазон памяти = 64КБ. Отсюда имеем первую проблему, придется дробить float64 и имитировать 16 битные числа. Можно не дробить, памяти у нас более чем достаточно. Хотя, в более новых стандартах, реализована работа с 32 и 64 битными числами, написанная на самом Форте. Можно это обыграть, используя стандартный функционал Lua. Еще из-за особенностей выравнивания памяти, у чисел с плавающей точкой отдельный стек и отдельная адресация. Это можно тоже игнорировать и запихнуть float'ы в стек данных (а может и нельзя, тут пока не понятно). Вообще, все это описывается самим Фортом, но имея уже готовый интерфейс к математическому сопроцессору, было бы глупо писать всякие sqrt/sin/tanh жонглированием на стеке. Еще стандарт ANS94 требует много лишнего, вроде доступа к ассемблеру, своеобразной работы с железом и мусорных функций. Поэтому, лучше видится стандарт FORTH-83, он описывает язык очень обобщенно. Только немного расширить его до реалий опенкомпов. Ссылки: стандарт 83 года FORTH-83, слова стандарт 94 года ANS94, слова краткое введение в синтаксис
  2. https://pastebin.com/4BXZP7FZ Использует шрифт Брайля Скрины: Пример:
  3. https://pastebin.com/LFAEzkP6 Делал погода назад. Откопал в бекапах.
  4. Среди всех компонентов в 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 while true do local chunk, err = handle.read() if not chunk then -- ⑥ local _, _, responseHeaders = handle.response() -- ⑦ local length for k, v in pairs(responseHeaders) do -- ⑧ if k:lower() == "content-length" then length = tonumber(v) end end if not length or read >= length then -- ⑨ break end return nil, ("error occured while reading response: %s"):format(err or "unknown error") -- ⑩ end read = read + #chunk -- ⑪ table.insert(url, chunk) end return table.concat(url) -- ⑫ end ① — открываем файл для чтения. Обрабатываем ошибки. ② — считываем всё из файла. Не забываем закрыть его за собой. ③ — вызываем заранее написанную функцию asFormData. Мы получаем тело запроса и значение хедера Content-Type. Создаём таблицу хедеров. ④ — отправляем наш запрос. Обрабатываем ошибки. ⑤ — handle.read может не сразу вернуть весь ответ, а кусочками. Чтобы не забивать память кучей строк, кусочки мы будем класть в таблицу (получится что-то вроде {"htt", "p://", "clbi", "n.co", "m/ab", "cdef"}). Также мы храним число прочитанных байт. ⑥ — handle.read может ещё вернуть ошибку. В том числе если мы прочли весь ответ, и сервер закрыл соединение. Поэтому обработка ошибок будет немного сложной. ⑦ — мы хотим сверить число прочитанных байт с размером ответа. Для этого нам потребуется получить хедеры, отправленными сервером. Вызываем handle.response. ⑧ — размер ответа обычно пишется в заголовок Content-Length. Однако сервер может поиграться с регистром. Например, писать content-length или CONTENT-LENGTH. OpenComputers не трогает эти хедеры. Поэтому придётся пройтись по всем ключам таблицы и найти хедер без учёта регистра. ⑨ — если length не nil, то это число. Тогда проверяем, что столько байт мы прочли. Если же Content-Length не задан, то будем считать, что серверу не важно, сколько надо прочесть. В любом случае выходим из цикла и завершаем чтение. ⑩ — если мы прочли меньше, чем требуется, то явно ошибка какая-то. Обрабатываем. ⑪ — не забываем обновлять read. ⑫ — наконец, склеиваем таблицу в одну строку. Из цикла можно выйти только в случае ошибки. А при ошибке соединение уже закрыто. Следовательно, самим вызывать handle.close() не нужно. 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 while true do local chunk, err = handle.read() if not chunk then f:close() -- ③ local _, _, responseHeaders = handle.response() local length for k, v in pairs(responseHeaders) do if k:lower() == "content-length" then length = tonumber(v) end end if not length or read >= length then break end return nil, ("error occured while reading response: %s"):format(err or "unknown error") end read = read + #chunk f:write(chunk) end 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. В прошлый раз я патчил OpenComputers, чтобы пробрасывать нативную либу debug. Пойдём дальше. Добавим нативных либ package и os. Прокинем дефолтное окружение внутрь песочницы. Пропатчим мод, чтобы можно было загружать си-модули. Загрузим профилятор и посмотрим, что из этого вышло. На винде ничего не заработает. Гарантирую. Если надо профилировать, ставьте нормальные оси или мучайтесь. 0. Сырцы мода Так как мы будем патчить мод, надо сначала подготовить исходники. $ git clone https://github.com/MightyPirates/OpenComputers.git $ cd OpenComputers $ git checkout master-MC1.12 $ ./gradlew setupDecompWorkspace На третьей строке версию выбираем по вкусу и выпекаем всё необходимое для компиляции. 1. Нативные либы Здесь всё просто. Открываем файл src/main/scala/li/cil/oc/server/machine/luac/LuaStateFactory.scala. Творим следующее: Вуаля. Теперь в machine.lua будут глобальные переменные package и _os. Отмечу отдельно, что меняем мы только архитектуру Lua 5.3. Уже на этом этапе у нас может сломаться персистентность. Это не страшно: она и должна сломаться. 2. Прокидываем окружение Поступаем аналогично тому, что делали в прошлой записи: меняем src/main/resources/assets/opencomputers/lua/machine.lua: Внутри песочницы в глобальной переменной env запечатлено будет всё окружение machine.lua. 3. C-модули Уже сейчас можно загрузить OpenOS и прописать env.require("libname"). Проблема в том, что C-модули так подключить не получится. Связано это с особенностью Lua. Абстрактно задача заключается в том, чтобы загрузить библиотку Lua с dlopen(..., RTLD_GLOBAL). System.loadLibrary в жаве флаг этот упускает по очевидным причинам, а нам он нужен. Значит, пришло время костылей. 3.1. Подключаем JNA: build.gradle Первый ханк нужен, чтобы можно было потом компилировать мод. Почему-то у курсов мавен не работает, а разбираться мне лень. 3.2. Патчим ещё раз src/main/scala/li/cil/oc/server/machine/luac/LuaStateFactory.scala Во-первых, подключаем хэшмапу. Потребуется. Во-вторых, импортируем JNA. Вернее, его часть. В-третьих, патчим код, чтобы он загружал Lua 5.3 через JNA. Магическая константа 0x101 — это значение RTLD_LAZY | RTLD_GLOBAL на моей системе. На фряхе, маке оно может отличаться. На этом этапе Lua 5.2 не будет работать. Включаться будет только Lua 5.3 из-за конфликта имён. Кроме того, JNA — это, вообще, огромная либа. Ради одной функции её подключать — это оверкилл. Но я в тонкостях JVM и JNI не силён. Как уже сказал, разбираться мне лень. 3.3. Компилируем $ ./gradlew assemble Выхлоп в build/libs. Берём жарник без суффиксов вроде -javadoc, -api, -sources. 4. Настраиваем профилятор Профилятор я написал сам на Rust. Вот ссылка: https://github.com/Fingercomp/lprofile-rs Очевидно, нам надо его скомпилировать. 4.1. Компилируем профилятор Ставим cargo (мультитул раста такой) любым удобным способом. Собираем: $ cd .. $ git clone --recurse-submodules https://github.com/Fingercomp/lprofile-rs.git $ cd lprofile-rs $ cargo build --release В target/release будет лежать liblprofile.so. Тырим его. 4.2. Определяем pwd Кидаем пропатченный OC в моды и запускаем игру. Пишем в опенкомпе env._os.getenv("PWD"), чтобы определить текущую директорию. Кидаем либу-профилятор в неё. 4.3. Профилируем Наконец, можно заняться мясом. local profiler = env.require("lprofile").Profiler() local result = profiler(function() local v = 0 for i = 1, 10e6, 1 do v = v + i end end) table.sort(result, function(lhs, rhs) return lhs.totalTime < rhs.totalTime end) print("Name", "# of calls", "Total time", "Total time, excluding inner calls") for _, v in ipairs(result) do print(("%s\t%d\t%.6f s\t%.6f s"):format(v.name, v.calls, v.totalTime, v.totalSelfTime)) end print("total time:", result.totalTime) 5. Зачем Мы получили наполовину сломанную версию OpenComputers: без Lua 5.3, без персистентности. Зато можем профилировать программы. Этот пост я написал, чтобы не забыть самому. Сомневаюсь, что кому-то интересно заниматься такой норкомагией.
  6. "Дело было вечером, Делать было нечего." С. В. Михалков Писал я себе спокойно программы на Java, но захотелось мне изучить C++. С этим я легко справился и подумал - что бы мне такое написать для практики? Решил написать что-нибудь с использованием какой-нибудь интересной библиотеки. Например Lua. Тут я и вспомнил про OC и решил накодить эмулятор. Не буду про него ничего писать - просто оставлю ссыль на реп - вот. Там в ридми все разжевано. Эмулятор написан на C++ с использованием библиотек lua и SDL. Код писал полностью сам, кроме поддержки юникода - библиотеку для этого я нашел на гитхабе и Crl+C Ctrl+V (ну лень было разбираться в этих кодировках). Эмулятор еще написан не до конца, но OpenOS на нем работает. Сейчас пишу поддержку интернет-карт. Да, я знаю о том, что этих эмуляторов уже хоть пруд пруди, но вдруг кому-то мой покажется лучше. В общем, пользуйтесь на здоровье.
  7. Fingercomp

    Улучшенный debug.debug()

    Сейчас я покажу, как сделать это: На скрине выше — улучшенный debug.debug(). Он умеет: Бегать вверх-вниз по стэку вызовов независимо от того, где запущен. Показывать красивые стэктрейсы. Имитировать динамический скоуп: получать значения локальных переменных, редактировать их, не требуя возни с либой debug. При этом учитывает, на каком уровне в стэке вызовов он находится. Он не умеет: «Шагать» по коду, заходить внутрь функций, проскакивать над ними. Таким образом, это не совсем дебаггер. Но он может показать состояние всех доступных переменных. Чтобы заюзать в коде, нужно сделать так: require("dbg")() Впрочем, если в проге есть какой-то часто вызываемый сегмент, то безусловно падать в мини-дебаггер на каждой итерации очень печально. Поэтому можно задать условие, при котором его запускать. Например: require("dbg")(nonNegative < 0) У нас есть переменная nonNegative, которая семантически всегда неотрицательна. Если ж внезапно попалось что-то меньше нуля, есть смысл попросить программиста проверить, кто (и как) изобрёл свою алгебру. Команды: :bt — показать стэктрейс. :up — прыгнуть на уровень вверх. :down — спуститься на уровень вниз. :frame N — перейти на N-ый уровень. Выйти из интерпретатора можно, нажав Ctrl-D или Ctrl-C. Код: https://gist.github.com/Fingercomp/58388304f45bf6b2b8108e3b7a555315 (задумывался одноразовым, качество соответствующее). В обычной Lua надо просто кинуть содержимое куда-нибудь, откуда require тащит файлы. Чтобы это работало в OpenComputers, придётся пропатчить содержимое мода: Открываем jar-файл мода в архиваторе. Идём в /assets/opencomputers/lua. Открываем файл machine.lua и в районе 971 строки делаем как-то так: Сохраняемся и выходим. Если всё сделано правильно, в OpenComputers теперь доступна полная либа debug. Остаётся закинуть код мини-дебаггера, например, в /home/lib, дальше используем как обычно. Очевидно, что на серверах такое делать не надо. Ну, совсем не надо. Полной либой debug легко выудить нативную load. А это уже уязвимость. Но в сингле вещь незаменимая. Цитирую отзыв пользователя, пожелавшего остаться анонимным: Успехов вам в дезинсекции кода.
  8. Программа позволяет создавать снимки экрана. Сохраняет она его в файл /screenshots/Screenshot-Год-День-Час-Минута.ast Формат ast Для выхода из программы после распечатывания экрана надо по нему нажать(создать ивент touch). Вот код "AsumScreenshoter"'а: local mode = ({...})[1] if mode==nil then print("Использование: screenshot init для создания ивента сохранения экрана(Кнопка Print Screen, папка screenshots)\n screenshot <имя файла> для просмотра(По окончанию просмотра нажмите на экран)"); os.exit() return end local fs = require("filesystem") local unicode = require("unicode") local gpu = require("component").gpu local event = require("event") local function dec2hex(n, s) local h = string.format("%x", n) return string.rep("0", s-string.len(h))..h end local function hex2dec(s) return tonumber("0x"..s) end local function createScreenshotFile() DD, HH, MM, SS = os.date("%Y"), os.date("%H"), os.date("%M"), os.date("%S") if not fs.isDirectory("/screenshots") then fs.makeDirectory("/screenshots") end return io.open("/screenshots/Screenshot-"..DD.."-"..HH.."-"..MM.."-"..SS..".ast", "w") end local function getScreen() local W, H = gpu.getResolution() local screenshot = dec2hex(W, 2)..dec2hex(H, 2) local lfg, lbg for y=1, H do for x=1, W do local symbol, fgcolor, bgcolor = gpu.get(x, y) if lfg ~= fgcolor then lfg = fgcolor screenshot = screenshot .. "␑" screenshot = screenshot .. dec2hex(fgcolor, 6) end if lbg ~= bgcolor then lbg = bgcolor screenshot = screenshot .. "␒" screenshot = screenshot .. dec2hex(bgcolor, 6) end screenshot = screenshot .. symbol end end return screenshot end local function writeScreen() local file=createScreenshotFile() file:write(getScreen()) file:flush() file:close() end local function printScreen(filename) local screenshot=io.lines(filename)() local oldw, oldh = gpu.getResolution() local header = screenshot:sub(1, 4) screenshot = screenshot:sub(5) local W = hex2dec(header:sub(1, 2)) local x = 0 local H = hex2dec(header:sub(3, 4)) local y = 0 gpu.setResolution(W, H) while y < H do y = y + 1 x = 0 while x < W do local sym = unicode.sub(screenshot, 1, 1) if sym == "␑" then gpu.setForeground(hex2dec(unicode.sub(screenshot, 2, 7))) screenshot = unicode.sub(screenshot, 8) elseif sym == "␒" then gpu.setBackground(hex2dec(unicode.sub(screenshot, 2, 7))) screenshot = unicode.sub(screenshot, 8) else x = x + 1 gpu.set(x, y, sym) screenshot = unicode.sub(screenshot, 2) end end end event.pull("touch") gpu.setResolution(oldw, oldh) end local function saveScreenByKey(_, _, key1, key2) if key1==0 and key2==183 then writeScreen() end end if mode=="init" then event.listen("key_down", saveScreenByKey) else printScreen(mode) end Надеюсь вы будете пользоваться этой программой!=) Для того,что бы начать делать скриншоты, после каждой перезагрузки надо запустить приложение с параметром init Для того,что бы создать скриншот после инициализации-надо нажать кнопку Print Screen
  9. Я видел статью про ООП (Объектно-Ориентированного Программирования), написанную на этом форуме, и вот, что могу сказать, он может быть немного непонятен новичкам в Lua. Я всегда пользовался другим способом создания "классов". Сначала следует разобраться со значениями. Класс - это набор методов (в нашем случае функций), процедур и переменных которые дальше будут наследоваться объектом или другим классом. Объектами называют сущности, обладающие набором свойств и операций над ними. Объект - это производное класса. В Lua классов как таковых нет, но если пофантазировать, то можно представить функцию как класс, а объект как таблицу. Создание простого псевдо-класса Наследование Экономия оперативной памяти Только читаемые свойства Вывод. Этот способ реализации ООП в Lua будет работать и в OC и в СС. "Классы" очень похожи на классы из других языков программирования. В моём варианте нет двоеточия, которое приносит странную возможность обработки одного объекта методом другого. Использовать ООП в игре можно для удобной разметки интерфейса в вашей программе (например для создания кнопок), и для экономии системных ресурсов в вашем компьютере внутри компьютера.
  10. Помню @@1Ridav разрабатывал мост для связи игры и ПК, чтобы можно было получать уведомления от роботов и компьютеров. Чуть посидев (а как вы помните я Lua не знаю) получилось вот это: https://pastebin.com/rZC8BZMs Что позволяет? Вы можете или запускать эту "штуку" просто с аргументом или вырезать те 2 строчки, которые принимают аргументы и просто включить этот код в код вашей программы и получать уведомления в телеграмм. Назвал я эту штуку TGInformer. Что нужно для работы? Со стороны Telegram: Добавить вот этого бота: https://t.me/ShowJsonBot, написать ему и в секции from найти id и записать его Добавить бота https://t.me/OC_InformerBot и написать ему любое сообщение. Со стороны OpenComputers: Компьютер с сетевой картой Загрузить код информера и открыть его для редактирования. Вверху в поле chatid впишите ID, который получили пунктами выше. Выполните tginformer test В результате в телеграм должно прийти test, а в игре код ответа: ОК Теперь осталось вписать это в свою программу и робот будет присылать уведомления типа я покакаль я выкопал ту большую яму P.S. Спасибо @@Alex за правки
  11. Fingercomp

    Про скобочки

    Продолжу рассказывать про знаки препинания. В этом посте — 3 разных истории про пару круглых скобок. 1. Вызовы функций Если функция вызывается с одним аргументом — строковым или табличным литералом, то скобочки необязательны. local function identity(x) return x end print(identity "test" == "test") print(table.unpack(identity {"test"}) == "test") Это чисто синтаксическая фишка, которая никак не влияет на исполнение кода. Очень удобно, чтобы вызвать функцию и передать ей таблицу с опциями. local logger = getLogger { name = "main", level = "info", output = {stdout}, } Если несколько литералов так разместить подряд, получится ряд последовательных вызовов: myFunc "hello" "world" {"how do you do"} -- myFunc("hello")("world")({"how do you do"}) Используя эту фичу, можно воплотить всякие норкоманские вещи. Как вам вот такой форматтер с интерполяцией? local myVar = 42 print(format "myVar = " {myVar} ", and I'm running " {_VERSION}) --> myVar = 42, and I'm running Lua 5.3 2. Ещё про литералы У всех строк есть метатаблица, у которой __index = string. Это значит, что можно вместо string.gsub(str, ...) писать str.gsub(str, ...), или str:gsub(...). Очень удобно, особенно последнее. Но вот просто так заменить str литералом нельзя. "test":gsub(...) — синтаксически неправильный код. Выручат скобки вокруг литерала: ("test"):gsub(...). Постоянно этим пользуюсь. Та же ситуация, если мы хотим проиндексировать табличный литерал: {foo = "bar"}.foo выдаст ошибку. Лечится аналогично: ({foo = "bar}).foo. Кроме индексации, скобочки нужны при вызове: вместо function() return 42 end() нужно писать (function() return 42 end)(). Наконец, есть ещё литералы численные: 42, например. В обычной Lua оборачивать их в скобки смысла, пожалуй, и не имеет, но с небольшим шаманством опять потребуются скобочки: debug.setmetatable(0, {__call = function(self) print(self) end}); (42)() --> 42 Правда, в OpenComputers отключён debug.setmetatable. 3. Функции с множественным выхлопом В Lua функция может вернуть несколько значений: local function test() return 1, 2, 3 end print(test()) --> 1 2 3 Однако бывает, что нужно достать только одно значение, а про остальные забыть. Для этого нужно обернуть в скобки вызов функции, вот так: print((test())) --> 1 Скобочки возьмут только первое значение и отбросят остальные. С помощью функции select можно выбрать и другое по счёту: local function identity(...) return ... end print((select(3, identity(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)))) --> 8
  12. Предисловие Я думал на новый сервер запилить прогу — мост между чатом сервера и IRC. У меня уже были такие программки: я насчитал минимум 6 различных версий мостов — каждая была немного переделанным клиентом IRC, который на дискете встроенной есть. Понять, в чём разница, даже с вимдиффом было сложно. Потому я плюнул и решил запилить полноценную ирколибу с красивой апишкой. Как это выглядит Вот полный код бота — моста. Сто двадцать шесть строчек. Прокомментирую некоторые из них. ① Подключаем либу и для укорачивания имён ещё вытаскиваем events, в которых хранятся все ивенты и priority. ② Создаем клиент с помощью билдера. ③ Через :connection задаём настройки соединения. Самое важное — адрес иркосервера. Порт обязателен. ④ Ирколиба знает меру в флуде. Опасаться, что бота выкосит флуд-фильтром, можно гораздо меньше. Это опционально, конечно. ⑤ Задаём ник бота, юзернейм и реалнейм. Юзернейм виден в хосте (nickname!username@domain.name), а реалнейм пишется в /whois. ⑥ Ирколиба умеет авторизовываться на сервере. Тоже опционально. ⑦ Эта группа выделена для ботоводческих настроек. Но пока там единственная опция — в какие каналы автоматически заходить. ⑧ Здесь задаются настройки исполнения. Опция threaded, по дефолту включённая, запустит бота в отдельном треде. Опция reconnect, также включённая по умолчанию, заставит бота переподключиться к серверу, если отвалится от него. Опция catchErrors перехватит ошибки в пользовательких листнерах; она отключена по умолчанию, чтобы не смущать. ⑨ Бот генерит ивенты для каждого сообщения. Так мы задаём обработчик для ивента. К слову, вместо функции здесь может быть корутина. ⑩ Есть и другие события. Например, irc.events.client.connected означает, что клиент соединился с сервером. А irc.events.client.authenticated говорит, что теперь можно слать сообщения. ⑪ Когда мы закончили конфигурировать бота, собираем через :build(). Если вместо него вызвать :buildAndRun(), бот тут же ещё и запустится. ⑫ Для удобства создадим ещё один тред, где будем работать с чатбоксом и ждать ^C. ⑬ Запускаем бота. Затем ждём завершения любого из двух потоков. ⑭ Когда это произошло, мы выключаем клиент, если он ещё подключен: тот выйдет с сообщением "Quitting." ⑮ Наконец, принудительно останавливаем потоки. На всякий случай. Красота ведь. Репозиторий Репа либы — на нашем гитлабе. Там же есть примеры использования и документация с описанием всего. Наконец, версия 1.0.0 лежит на хеле. Из-за баги в OC хпм крашиться может (фиксить лень), но можно попробовать скачать: $ hpm install libirc
  13. Нужен код, который будет объединять несколько файлов и папок в один файл и обратно.
  14. Некоторое время назад я публиковал программку - интернет-мост Stem. Он очень простой в использовании, но к сожалению пока не лишен некоторых глюков. А кроме того, у него есть недокументированные возможности. Предлагаю сейчас поиграться с одной такой тайной фичей. Это будет интересно тем, кто немного знаком с HTML/CSS/JavaScript. В чём суть? Всё просто. У Stem есть веб-клиент. То есть если вы зайдете по адресу https://stem.fomalhaut.me (например), вы увидите окно, которое пригласит вас ввести ID канала и початиться прямо с сайта. После ввода ID канала, вас перекинет на страничку с "чатом", где вы сможете посылать сообщения вашему компьютеру/роботу в майнкрафте. Адрес этой странички будет выглядеть примерно так: https://stem.fomalhaut.me/channel?id=test Где test это ID вашего канала. Когда робот будет вам отвечать, это будет видно в логе. Примерно так: А теперь - тайная фича. Находясь на страничке канала, как на скриншоте, вы можете приписать к адресу дополнительный флаг: &render=true. Полный адрес станет выглядеть примерно так: https://stem.fomalhaut.me/channel?id=test&render=true И начиная с этого момента, все входящие сообщения от компьютера в OpenComputers будут не отрисовываться в лог, а попадать в JS функцию eval(...). Что это значит? Те кто имел дело с JS уже наверное поняли все последствия. Но я поясню. Это значит, что компьютер OpenComputers может прислать сообщение с JS кодом, и сайт Stem этот код выполнит. А это значит, что вы можете творить на сайте вообще всё что угодно. Вы можете удалить интерфейс, показать вместо него новый, скачать что-то с инета, запустить игрушку... да хоть майнить крипту. Поиграем Ничего вредоносного мы делать конечно сейчас не будем. Вместо этого, традиционно, попробуем сделать так, что сайт Stem превратится в сайт для управления светом в нашем доме в Майнкрафте. Шаг первый. Подготовим дом. Тут всё просто - стандартный компьютер, с интернет картой, от него кабель к I/O блоку, а на блоке - подопытная лампа. На компьютер ставим Stem. Например, ставим HPM такой командой, pastebin run vf6upeAN И потом пишем: hpm install stem Нажимаем Enter и готово. Шаг второй. Нам нужен сайт. Усложнять не будем, и наш революционный дизайн будет выглядеть так: Что нам нужно сделать, чтобы сайт Stem превратился в то что нам надо? Методом тыка, через консоль браузера определяем, что достаточно выполнить такой код: document.body.innerHTML = " <style>button:active { background: #fac700; }</style> <button onclick=\"sendMessage(\'toggle\')\">Toggle Light</button> "; document.body.style.alignItems = "center"; Первой строкой мы просто заменяем всё содержимое тега <body> на то что нам надо. Я добавил еще немного косметики в виде стиля для кнопки. Вторая строка - тоже косметика, я просто поправил стиль на теге <body> чтобы кнопочка была посередине. Один важный ньюанс - на кнопке повешено свойство onclick с кодом sendMessage("toggle"). Таким образом, когда пользователь тыкнет по кнопке, мы отправим сообщение обратно в OpenComputers. Функцию sendMessage нам дает веб-клиент Stem. Ей можно безвозмездно пользоваться в своих целях. Шаг третий. Соединяем это все вместе. Теперь надо набросать программку для нашего компьютера в Майнкрафте, чтобы он выслал подготовленную JS-"бомбу" по нашему сигналу. Эники, беники... выходит что-то такое: local event = require('event') local stem = require('stem') local side = require('sides') local com = require('component') local redstone = com.isAvailable('redstone') and com.redstone or nil local channel = "test" local layout = [===[ document.body.innerHTML = "\ <style>button:active { background: #fac700; }</style>\ <button onclick=\"sendMessage(\'toggle\')\">Toggle Light</button>\ "; document.body.style.alignItems = "center"; ]===] local lampSide = side.top local lampTurnedOn = false print("Connecting to the #" .. channel .. " STEM channel...") local server = stem.connect('stem.fomalhaut.me') server:subscribe(channel) print("Done.") print("Press [q] to quit.") while true do local name, a, b = event.pull() if name == "stem_message" then local message = b if message == "connect" then print("Someone wants to connect - sending the layout...") server:send(channel, layout) elseif message == "toggle" then if redstone ~= nil then if not lampTurnedOn then redstone.setOutput(lampSide, 16) lampTurnedOn = true else redstone.setOutput(lampSide, 0) lampTurnedOn = false end end end elseif name == "key_down" then local char = b if char == 113 then break end end end server:disconnect() Я думаю тут большая часть очевидна и понятна. Мы подключаемся к серверу Stem, подписываемся на нужный канал и внимательно слушаем входящие сообщения. Когда пользователь присылает сообщение connect, мы отправляем ему подготовленный код, который мирно лежит в переменной layout. Этот код прилетит к пользователю, и, если пользователь смотрит страницу с включенной опцией render=true, код сработает и перерисует страницу. Если опция будет отключена - он просто увидит код в логе, как простое сообщение. Шаг четвертый. Тестируем. Откроем наш канал по ссылке: https://stem.fomalhaut.me/channel?id=test&render=true Появится пустой лог. Запустим программу в OpenComputers. Она отрисует наше приветствие, что-то вроде: Сonnecting to the #test STEM channel... Done. Press [q] to quit. Теперь пишем на сайте команду connect. Если мы все сделали правильно, и Stem не заглючил, интерфейс сайта исчез, и вместо него появилась наша кнопка. Нажимаем её. Вуаля! Дома зажегся свет. Постскриптум Это конечно же недокументированная хакерская фича, которую можно считать альфа-версией. Кроме того в Stem сейчас есть неуловимый баг, из за которого сообщения иногда не доходят. Не пугайтесь. Если кто-нибудь предоставит мне сценарий (прямо по шагам), при котором 100% срабатывает баг - буду очень рад и пофиксить его будет легче. С помощью описанной фишки можно придумать много хрени. Я показал самое простое что пришло в голову. Уверен, ваша фантазия будет покруче моей ) Пишите баг-репорты или пожелания по фиче, да и просто комменты в эту тему, либо мне в любой канал связи, где я онлайн.
  15. ; — это код, который делает приблизительно ничего. Не абсолютно: об этом статья. Когда я писал crateriform (видяшки в гайде про корутины этим набором прог зарендерены), я отталкивался от Lua-парсера на Lua от @LeshaInc (спасибо ему ещё раз): Lua-часть принимала исходный файл с кодом, парсила его на AST и генерировала по нему обратно код. С костылями. Для рендера нужен был "сценарий" — файл, в котором указаны, какие символы изменились и на какое значение, помимо прочего. Генерируемые костыли как раз этим занимались: они оборачивались вокруг выражений и писали в файл высчитанное значение. Чтобы показать кадр с "n = 10", как на гифке, и следующий за ним, сценарий такой: 5,16 5,16 expr 10 Здесь через пробел: позиция начала в исходном коде, позиция конца, опкод (expr) и значение выражения. Так как генерируемый код делает то же, что и исходный, но ещё рисует сценарий, а выражения могут быть засунуты где угодно и раскрываться сразу в несколько значений, я создаю лямбду (анонимную функцию) и тут же её вызываю. Вот как выглядит часть сгенерированного кода, который занимается строкой выше: highlight({5, 16}, {5, 16}, (function(...) local values = table.pack(...) return function() return table.unpack(values, 1, values.n) end end)((n)), 'expr') Выглядит очень сложно (для сложности есть причины), но сейчас интересует только это: (function(...) <...> end)(...) Эта конструкция присутствует как третий аргумент к highlight. Программисты на JS с этим шаблоном должны быть знакомы: создаётся лямбда и тут же вызывается с некими аргументами. В месте, где синтаксис позволяет указывать только выражение — в списке аргументов вызова в нашем случае, — таким образом размещаем стейтменты. На всякий случай скажу, что стейтмент — любая цельная конструкция, кроме выражений. Если вы берёте часть кода, которая отдельно, вырванная из кода, не выдаёт синтаксическую ошибку (например, цикл for или local x = 3), и её нельзя поместить после x = , это стейтмент. А теперь суть. (function(x) print(x^2) end)(2) (function(y) print(y + 2) end)(2) Этот код задумывался так, чтобы он два раза принтнул четвёрку. Запускаем: $ lua5.3 semicolon.lua 4.0 lua5.3: semicolon.lua:1: attempt to call a nil value stack traceback: semicolon.lua:1: in main chunk [C]: in ? ..? Lua игнорирует пробельные символы. Код вроде такого: func(3) (4)(5) ...эквивалентен такому: func(3)(4)(5) В примере с ошибкой вызывается первая лямбда, возвращающая nil, который мы затем пытаемся вызвать с аргументом — второй лямбдой. Поэтому получаем "attempt to call a nil value". Чиним с помощью ;: (function(x) print(x^2) --> 4.0 end)(2) ; (function(y) print(y + 2) --> 4 end)(2) Кстати, чтобы ещё с толку сбить: комментарий вместо ; ошибку не исправит. Ещё один ошибочный пример: local pi = math.pi (function(r) print(2 * pi * r) end)(3) Как чинить, вы уже знаете. Отдельно упомяну точку с запятой после return. return обязан быть последним стейтментом в блоке. А ; — это стейтмент. Почему тогда можно делать так? local function f(x) return x^2; end print(f(2)) --> 4.0 Ответ: потому что ; — это опциональная часть return, а не отдельный стейтмент. Если же залипает клавиша и получается вот так: local function f(x) return x^2;; end print(f(2)) ...то будет синтаксическая ошибка. Вторая точка с запятой — теперь отдельный стейтмент, которых после return быть не должно. Поэтому с уважением относитесь к этому стейтменту. Точка с запятой делает приблизительно ничего, но с умом.
  16. GPS это куча оборудования, трата энергии и времени, но иногда может понадобится там, где нет встроенного акселерометра, как у робота. Попробуем разобраться, как настроить и построить сеть спутников, по которым можно вычислить свои координаты. Где-то была готовая программа, но я ее не нашел, поэтому восстановил основной функционал, по примеру КомпутерКрафта. Для начала нужен компьютер с установленной OpenOS. Скачиваем для него программатор хостов: pastebin get ZD8GysNP /bin/fgps.lua Эта программа позволяет устанавливать прошивку для спутников-микроконтроллеров, которые будут рассылать свои координаты по запросу. Далее, соберем несколько микроконтроллеров с беспроводными картами, при необходимости обеспечим их энергией. Теперь надо установить прошивку для первой партии: узнаем координаты, где будет стоять очередной хост вставляем EEPROM в компьютер запускаем программатор, с указанием координат через пробел (например: fgps 1652 64 -152) Прошитые карточки вставляем в микроконтроллеры (в сетке крафта) и расставляем в соответствии с координатами, указанными при прошивке. Самое главное - не перепутать координаты. Если навести курсор на микроконтроллер и зажать LSHIFT, то будет виден лейбл биоса, который является подсказкой для его позиции. Когда первая партия размещена и активированна, остальные хосты можно прошивать без указания координат - они определят позицию сразу при включении (если они будут в радиусе действия четырех рабочих хостов) Но лучше, конечно, чтобы каждый хост знал свои точные координаты. Ибо в случае небольшой ошибки в начале постройки, по мере расширения сети, погрешность будет увеличиваться, т. к. поправка будет производится по неверно вычисленным координатам хостов. Теперь проверим работу системы в деле. Возьмем, например, планшет с беспроводным модемом. Скачаем библиотеку GPS: pastebin get Dja9ZceV /lib/gps.lua Запустим консоль Lua. Загрузим библиотеку: local gps = require('gps') И попробуем получить координаты: gps.locate(true) (параметр DEBUG передаваемый в функцию работает как и в ComputerCraft - выводит все вспомогательные сообщения) Каждый хост, при старте устанавливает wake-up сообщение и включается только чтобы ответить на запрос координат. Такой подход экономит огромное количество энергии и позволяет добиться полной автономности хостов - встроенной солнечной панели вполне хватает для работы. Для стабильной работы надо будет добавить data-карту и реализовать протокол управления спутниками по прошитому открытому ключу, чтобы удобно было прошивать новые координаты при перемещении хоста на новое место.
  17. Кто нибудь знает, есть ли интерпретатор вне майнкрафта для lua OpenComputers дабы более удобно проверять и дебажить код ибо в самом майнкрафте это крайне не удобно делать.
  18. Fingercomp

    Прокачка стектрейса

    Эта беспрецедентно короткая запись имеет начало своих ног в запросе @Laine_prikol, как-то спросивший в нашей ирке, можно ли стэктрейс сделать не таким тупым. Меня это заинтересовало, и спустя часик выросла очень короткая программка, которая рисует вот такие стэктрейсы: # 0: C field function yield(...) (defined in [C]) # 1: Lua local function f(f=function: 0x559402b83590, a=42, b=24, vararg test, nil):109 (defined in trace.lua at L108) # 2: Lua local function outer(f=function: 0x5594040b2230, g=function: 0x559402b83590, a=42, b=24, <... (1 arg)>):105 (defined in trace.lua at L103) # 3: Lua function <anon>():113 (defined in trace.lua at L102) Заметили что-то необычное? Наконец-то пишется, какие аргументы имеются у функции, потому что это куда информативнее беглому взгляду, чем описание расположения и строки. Код лежит на гисте: https://gist.github.com/Fingercomp/a688d221356cb371d940b947d0ca90a8. Использованы функции debug.getinfo и debug.getlocal. Аргументы должны писаться даже внутри OC, но уже без значений.
  19. @Krutoy когда-то писал про IDE для Lua, но с тех пор, я надеюсь, всё достаточно преобразилось, чтобы снова подошло время задать следующий вопрос: «Какие есть окружения разработки для Lua?» Желательно интегрированные. Иными словами: редактор кода полноценный (само собою); автодополнение (уже никуда без этого); дебаггер; поддержка множества окружений Lua (так, чтобы можно было и OpenOS встроить); неубогий вид; линтер, автоформаттер; статический анализатор кода; инструменты рефакторинга; сборка, запуск, деплой; конфигурируемость и удобность. Неплохими примочками была бы ещё поддержка C, C++ на уровне, но здесь, впрочем, можно и CLion/Eclipse поставить. Если есть опыт использования, интересно было бы услышать его тоже. Например: vim — из списка только 2 пункта есть: первый и последний. Творить лютую жесть можно (для однотипного изменения есть и реплейс по регексу полноценный, и макросы, которыми я довольно часто пользуюсь), к тому же, он легковесный и шустрый очень. Но без остальных пунктов как-то печально писать код.
  20. Как вы уже видели, программа бесполезная. Просто поставьте комп с видеокартой 2 уровня, большой монитор 2 уровня где нибудь на улице, и установите на него OpenOS. Вы можете редактировать текст, изменяя данные в таблице (она в коде, не зря меня Монстрик учил). Ну что, разбираем. http://pastebin.com/PwhkZTb0
  21. Дело было вечером, делать было нечего... И тут я случайно зажал горячую клавишу калькулятора, и меня осенило! А ведь калькулятора то для ОС никто не писал еще! Так появилась идея создания этого калькулятора. Что можно про него сказать? Базовый калькулятор который умеет выполнять стандартные математические операции. Требования к ОС: Видеокарта 2 уровня и выше Монитор 2 уровня и выше Компьютер/сервер любого уровня с установленной OpenOS Управление самое простое - мышкой. При создании программы была взята статья Псевдографические интерфейсы в OpenComputers и на основе кода из неё был создан калькулятор. Установка: version 0.2 pastebin -get PaVaRGcd Calculator.lua version 0.3 pastebin -get ZjCTrj5a Calculator.lua P.S. Выражаю благодарность @Doob за статью. Если найдете какие-то баги, недочеты, или возникнут идеи по улучшению, я всех с радостью выслушаю.
  22. Всем здрасти. Скрины: Клиент 1: Клиент 2: Лог сервера: Реализовано: Выдача IP Передача данных по выданым IP Регистрация/удаление доменов Получение IP адреса по домену То что хочется реализовать: Улучшить защиту, возможно сделать шифрование -Улучшить способ передачи данных между внутриигровыми IP Сделать библиотеку для пользовательских программ(С++, Java, Python, итп) для управления\получения данных OC через сервер, а так же поддержку Arduino(Можно будет выводить значения на экран подключеный к Arduino) Добавить поддержку децентрализации если в сети более 1 сервера(Можно будет делать межсерверные DNS-запросы) (Продолжение следует) Плюсы: Можно соединять OC компьютеры стоящие на разных MC серверах Быстрая работа сервера Минусы: Нужна оптимизация и поиск багов Плохая защита Исходники GitHub: https://github.com/TheConnBit/OpenComputersDHCP-DNS P.S. Там же объяснение всех команд, итп Исходники для ленивых (обновлено): Сервер (Запускать через консоль java -jar и лучше в отдельной папке): DHCP-DNS-Server.jar Клиентская библиотека: ddns.lua На скрине показано: подключение к серверу, выдача IP, регистрация домена, запрос IP домена, передача данных, получение, удаление домена, отключение Вообщем, сильно не пинайте за код. Если кто возьмется искать\найдёт баги или будут предложения что улучшить\добавить, пишите мне сюда или на почту bithovalsky@gmail.com Сначало была идея только DHCP сервера, но потом я накнулся на статью Programmist135: http://computercraft.ru/topic/1853-dns/ и реализовал DNS. Последние изменения: - - v1.1 Исправлены названия функций Исправлен перевод Функция Resolve теперь возвращает IP Пофикшены отключения сервером клиентов из за таймаута Echo запроса Список доменов теперь указывается в файле config.properties Изменён способ отправки сообщений по IP, появились порты --Bit
  23. С новым годом, обитатели computercraft.ru! Сегодня, вашему вниманию я представляю игру "Атака бактерий мутантов v0.2". Предыстория: На космическом корабле проводились опыты, по выращиванию бактерий в агрессивной среде. Но случилась авария и космический корабль постигла страшная участь разгерметизации. И лишь сильнейшие бактерии, на кусочке стекла, остались одни в космосе. Смогут ли они адаптироваться? Смогут ли эволюционировать? Смогут ли выжить в этой агрессивной среде? Решать вам! Концепция: Игра разделена на этапы. На данный момент доступно только 2 этапа. 1 этап - мы управляем синий бактерией которая путём реактивного движения должна набрать нужную массу поглощая других бактерий. (в космосе, потом в воде на планете) 2 этап - бактерии научились ходить (действия происходят на планете, на суши) 3 этап - бактерии напали на местных жителей планеты и управляют их сознанием. Могут ПКМ переселяться из тела в тело. Нужно переселяться от меньшего противника к большему, пока те не перестреляли друг друга. В это время происходит гражданская война и инопланетяне друг друга убивают из бластеров стреляющих антивеществом. 4 этап - планета захвачена, нужно управляя телом инопланетянина участвовать в гражданской войне за звание лидера планеты. 5 этап - отбить атаку других инопланетян. Можно кататься в танке. Можно отдавать приказы и управлять союзниками. В прочем, планы на будущее могут сильно меняться. Вполне возможно что эволюция бактерий будет происходить по другому пути чем задумана сейчас. Проблема недостатка художников может совсем свести все планы на нуль. Начиная с 3 этапа нужно рисовать спрайты персонажей, а это не все могут. Что есть в игре: Реалистичная физика 2 режима игры (будет больше) Несколько тестовых уровней, чтобы проверить работоспособность игры (но можно добавить сколько угодно) Параллакс скроллинг планет на фоне Разноцветные, реалистичные звёзды на фоне Логотип и его анимация Миникарта Режим разработчика Менюшки Рабочие настройки (правда не сохранятся при перезапуске) Лёгкое добавление уровней и изменение их свойств Тестовая консоль Антивещество Зум Горячие клавиши во время игры: i - режим разработчика, включаются всякие индикаторы полезные для тестов m - отключаемая миникарта e - переход на следующий уровень, если набрана нужная масса ` - включить тестовую консоль (используется для выявления багов) Управление: Колесо мыши - зум ЛКМ в первом режиме игры даёт выброс массы (реактивное перемещение) Скрины: Видео обзор: (Видео затянутое. Для просмотра геймплея проматывайте до второй части, 1 минуты) Установка: Распаковываем архив и играем. Ссылка на архив: https://yadi.sk/d/1GBAgr71moH2s От разработчиков: Разработку ведут 2 человека: qwertyMAN и electronic_steve + один художник, предоставивший нам изображения планет. Игра позиционирует себя как клон игр Osmos, Spore и Tasty Planet. Написана на lua, с использованием движка love2d. В игре присутствуют заготовки на будущее: Индикатор прогресса в режиме разработчика, свойство наличия ИИ, которое пока не используется в клетках.
  24. В луа можно обрезать строку? Например мне нужно читать сообщения из чата с помощью чат-бокса, искать в сообщении плохие слова и выдавать куда-то уже сообщения без плохих слов. В библиотеке string ничего не нашёл((
  25. Можно ли в чистой Lua (ну или той которая используется в OC) сделать так чтобы две функции выполнялись ДЕЙСТВИТЕЛЬНО одновременно?
×
×
  • Создать...