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

Блоги

Важные записи

  • Fingercomp

    Обновление OpenComputers до версии 1.7.3

    Автор: Fingercomp

    Предлагаю поглядеть на новое обновление мода. Очень толстого обновления. Отрегулировали частоту выполнения хука, который шлёт этот ненавистный "too long without yielding", так что теперь и скорость исполнения кода должна гораздо возрасти, и с ошибкой этой код падать реже. Мы проверяли: некая гуи-либа с 1.6 fps до 2.5 fps только благодаря этому работать стала. Оптимизировали производительность ещё и записи на диск. Пошустрее будет — обещают, что в 5–500 раз. Сетевой разделит
    • 9 комментариев
    • 1 645 просмотров
  • Fingercomp

    OpenComputers 1.7.5

    Автор: Fingercomp

    О прошлой версии я умолчал, но исправляюсь. Вышла 1.7.5 с чаем и сладкими фичами.   Новинки Анализатор, которым адреса компонентов получаем, теперь вставляется в планшет. Он займёт компонент barcode_reader, но методов у него нет. Зато он вернёт в ивенте tablet_use адреса и типы всех компонентов внутри блока, если планшетом нажать на него и удерживать до писка. Известно, что в компы вставлять можно любой объём текста не более 256 строк. Дело в том, что из-за ошибки каждая
    • 9 комментариев
    • 605 просмотров

Блоги сайта

  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 параметра:

    1. 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/.
    2. Данные запроса. Оно же тело запроса. Если мы отправляем GET/HEAD-запрос, то этот аргумент надо установить в nil.
    3. Хедеры, которыми запрос сопровождать. Можно поставить nil, тогда там по минимуму дефолтные подтянутся. Иначе передавать надо таблицу. Её ключи — это названия хедеров. Например, {["Content-Type"] = "application/json"}.
    4. Метод запроса. Если же этот аргумент не передавать, то возьмётся по дефолту GET или POST: это зависит от того, пуст ли аргумент 2 или нет.


    Если возникла ошибка, метод вернёт nil и сообщение об ошибке.

    Если же всё нормально, то метод вернёт handle — табличку с функциями. Вот что это за функции:

    1. handle.finishConnect() — проверяет, подключены ли мы к серверу.
      • Если да, то вернёт true.
      • Если к серверу ещё не подключены, то вернёт false.
      • Если же возникла ошибка (например, 404 вернул сервер или закрыл соединение), то вернёт nil и сообщение об ошибке. Например, nil, "connection lost".
      • В доках написано, что функция ошибку пробрасывает. На самом деле нет: она вообще не бросает исключения.
    2. handle.response() — возвращает мета-данные ответа с сервера.
      • Если соединение ещё не установлено, вернёт nil.
      • Если возникла ошибка, вернёт nil и сообщение об ошибке. Например, nil, "connection lost".
      • В противном случае возвращает 3 значения:
        1. Код ответа (например, 200).
        2. Статус (например, "OK").
        3. Таблицу с хедерами, которые отправил сервер. Выглядит примерно так: {["Content-Type"] = {"application/json", n = 1}, ["X-My-Header"] = {"value 1", "value 2", n = 2}}. Выпишу отдельно, что значения таблицы — это не строки, а ещё одни таблицы.
    3. handle.read([n: number]) — читает n байт (если n не задано, то сколько сможет).
      • Если компьютер ещё не успел получить данные, то отдаст "".
      • Если возникла ошибка, то выдаст nil и сообщение об ошибке. Например, nil, "connection lost".
      • Если сервер закрыл соединение, то вернёт nil.
      • В противном случае отдаст строку с частью ответа.
    4. handle.close() — закрывает соединение.

     

    2. TCP-сокеты: component.internet.connect

    У метода есть 2 параметра:

    1. Адрес хоста. Например, 127.0.0.1. Здесь также можно указать порт: google.com:80.
    2. Порт. Если в первом аргументе порта нет, то второй параметр обязателен.

     

    Если возникла ошибка, он также вернёт nil и сообщение. Иначе возвращает handle — табличку с функциями. Вот такими:

    1. handle.finishConnect() — то же, что и выше.
    2. handle.read([n: number]) — то же, что и выше.
    3. handle.write(data: string) — отправляет data по сокету на сервер. Возвращает число переданных байт.
      • Если соединение не установлено, это число равно 0.
    4. handle.close() — то же, что и выше.
    5. handle.id() — возвращает id сокета.

     

    3. Как правильно отправить HTTP-запрос на сервер и получить ответ

    Чтобы было интереснее, реальная задача: написать аналог pastebin, только вместо пастбина использовать https://clbin.com/. Особенности:

    1. Для взаимодействия с сайтом нужно отправлять HTTP-запросы: GET и POST. Это всё OC умеет.
    2. Чтобы скачать, достаточно простого GET по ссылке. Это можно сделать даже через wget.
    3. А вот чтобы отправить файл, надо использовать MIME-тип multipart/form-data. OC не умеет из коробки такие формы отправлять. Мы напишем минимальную реализацию, которая бы нас устроила.
    4. Не забываем, что этот MIME-тип нужно установить в хедер.
    5. При этом мы хотим красиво обработать все ошибки и не допустить ошибок сами.

    Таким образом, использовать будем практически все фичи.

     

    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

    Эту функцию можно прямо брать и копипастить в свои программы. Что она делает:

    1. ① — отправляем запрос. Сразу обрабатываем ошибку.
    2. ② — запрос доходит до сервера не мгновенно. Нужно подождать. Чтобы не зависнуть слишком долго, мы засекаем время начала.
    3. ③ — вызываем finishConnect, чтобы узнать статус подключения.
    4. ④ — finishConnect вернул true. Значит, соединение установлено. Уходим из цикла.
    5. ⑤ — finishConnect вернул nil. Мы специально проверяем через status == nil, потому что не нужно путать его с false. nil — это ошибка. Поэтому оформляем его как ошибку.
    6. ⑥ — проверяем, висим ли в цикле мы слишком долго. Если да, то тоже возвращаем ошибку. Не забываем закрыть за собой соединение.
    7. ⑦ — нам не нужен бизи-луп. Спим.
    8. ⑧ — мы не читаем сразу всё в память, чтобы экономить память. Вместо этого отдаём наружу 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
    1. ① — открываем файл для чтения. Обрабатываем ошибки.
    2. ② — считываем всё из файла. Не забываем закрыть его за собой.
    3. ③ — вызываем заранее написанную функцию asFormData. Мы получаем тело запроса и значение хедера Content-Type. Создаём таблицу хедеров.
    4. ④ — отправляем наш запрос. Обрабатываем ошибки.
    5. ⑤ — handle.read может не сразу вернуть весь ответ, а кусочками. Чтобы не забивать память кучей строк, кусочки мы будем класть в таблицу (получится что-то вроде {"htt", "p://", "clbi", "n.co", "m/ab", "cdef"}). Также мы храним число прочитанных байт.
    6. ⑥ — handle.read может ещё вернуть ошибку. В том числе если мы прочли весь ответ, и сервер закрыл соединение. Поэтому обработка ошибок будет немного сложной.
    7. ⑦ — мы хотим сверить число прочитанных байт с размером ответа. Для этого нам потребуется получить хедеры, отправленными сервером. Вызываем handle.response.
    8. ⑧ — размер ответа обычно пишется в заголовок Content-Length. Однако сервер может поиграться с регистром. Например, писать content-length или CONTENT-LENGTH. OpenComputers не трогает эти хедеры. Поэтому придётся пройтись по всем ключам таблицы и найти хедер без учёта регистра.
    9. ⑨ — если length не nil, то это число. Тогда проверяем, что столько байт мы прочли. Если же Content-Length не задан, то будем считать, что серверу не важно, сколько надо прочесть. В любом случае выходим из цикла и завершаем чтение.
    10. ⑩ — если мы прочли меньше, чем требуется, то явно ошибка какая-то. Обрабатываем.
    11. ⑪ — не забываем обновлять read.
    12. ⑫ — наконец, склеиваем таблицу в одну строку.

     

    Из цикла можно выйти только в случае ошибки. А при ошибке соединение уже закрыто. Следовательно, самим вызывать 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
    1. ① — открываем файл, в этот раз для записи. Обрабатываем ошибки.
    2. ② — отправляем запрос без данных и с дефолтными хедерами. Обрабатываем ошибки.
    3. ③ — если мы сюда попали, то дальше каким-либо образом (ретурном или брейком) выпрыгнем из цикла. Поэтому не забываем закрывать за собой файл.

     

    Чтобы было удобнее копипастить, я оставил повторяющийся код в двух функциях. В своей программке можно 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. Напишем для него клиент. Правильно. Опять же, особенности:

    1. Работает через TCP-сокет.
    2. Протокол бинарный.
    3. И асинхронный.
    4. А ещё сессионный: у каждого TCP-соединения есть собственный стейт.
    5. Доки хранятся на вики.
    6. При разрыве соединения клиент должен переподключиться и восстановить стейт.

     

    Здесь снова придётся использовать все фичи интернет-карты.

     

    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,
      },
    }
    1. ① — я использую обёртку над component.internet. Она потом будет нужна, чтобы мы могли поместить сокет в буфер. Обращаю внимание, что вызов обёрнут в assert. Работает она так: если первое значение не nil и не false, то возвращает его, а иначе кидает ошибку, используя второе значение в качестве сообщения. Проще говоря, она превращает nil, "error message" в исключение.
    2. ② — а пока я вытягиваю из обёртки сокет...
    3. ③ — чтобы можно было проверить, установлено ли соединение. Код здесь аналогичен тому, что мы делали в прошлой секции. Не выдумываем.
    4. ④ — одно различие: вместо return nil, "error message" я сразу прокидываю исключение. Прежде всего потому, что ошибки мы прокидывать должны единообразно. Раз в ① кидаем исключение, и здесь делаем то же.

      Почему исключение, а не return nil, "error message"? Мы вызывать connect будем из всяких мест. Так как в случае ошибок бэкенд беспомощен, то лучше прокинуть ошибку до фронтенда и не усложнять код бэка проверками на nil. Кроме того, это громкая ошибка: если забыть где-то её обработать, она запринтится на экран, случайно пропустить её или подменить какой-нибудь непонятной "attempt to index a nil value" не получится.

      В конце концов, мне так проще.
    5. ⑤ — сокет я сохраняю в поле. socket.finishConnect нам ещё понадобится.
    6. ⑥ — пришло время обернуть сокет в буфер. Может показаться излишним, особенно учитывая ⑧. Причины станут ясны, когда будем делать чтение.

      rw — это буфер для чтения и записи. b — бинарный режим: buffer:read(2) вернёт 2 байта, а не 2 символа. Так как символы кодируются в UTF-8 и занимают 1 (латиница), 2 (кириллица, диакритика), 3 (BMP: куча письменностей, всякие графические символы, большая часть китайско-японско-корейских иероглифов) или 4 байта (всё, что не влезло в BMP, например emoji), то отсутствие этого режима может дать ощутимую разницу. В нашем случае протокол бинарный — ставим b.
    7. ⑦ — устанавливаем таймаут для чтения. Объясню подробнее, когда будем это чтение делать.
    8. ⑧ — отключаем буфер для записи. Он нам не нужен.
    9. ⑨ — здесь же подключаемся ко всем каналам.

     

    Итого мы получаем свойства __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,
      }
    }
    1. ① — рассмотрим эту мишуру по порядку:
      • Любой пакет stem начинается с 2 байт, которыми кодируется длина остатка. Отсюда всплывает двойка.
      • Автор buffer, к сожалению, не осилил реализовать адекватную обработку ошибок. Он использует и исключения, и тихие ошибки (nil, "error message").
      • В случае таймаута будет прокинуто исключение. Однако мы перед чтением поставили таймаут в 0. Если буфер не найдёт сразу 2 байта в сокете, то он сразу кинет ошибку.
      • Мы хотим проверить, есть ли в сокете пакет, который бы можно было прочесть. Используем pcall. Сначала раскроем self.__buffer:read(2) как self.__buffer.read(self.__buffer, 2), а затем поместим функцию и её аргументы в pcall.
      • pcall возвращать будет сразу 3 значения по следующему принципу:
        1. Если на сокете есть 2 непрочитанных байта, read вернёт их без ошибок. Тогда status будет равен true, в head сохранятся эти 2 байта, а в err запишется nil.
        2. Если на сокете этих байтов нет, то read прокинет исключение "timeout". status установится в false, head приравняется "/lib/buffer.lua:74: timeout", а err также будет nil.
        3. Если же при чтении с сокета возникла другая ошибка, то read вернёт её по-тихому: status будет true, head — nil, а сообщение об ошибке уйдёт в err.
        4. Не думаю, что этот случай возможен, однако read может кинуть исключение и не из-за таймаута. status установится в false, а ошибка сохранится в head.
      • В if мы проверяем, был ли таймаут (ситуация 1.2). В таком случае мы не кидаем исключения, а тихо выходим.
      • Наконец, не забываем вернуть прежнее значение таймаута.
    2. ② — обрабатываем случай 1.4.
    3. ③ — обрабатываем случай 1.3 с помощью assert. Последний оставшийся и единственный успешный случай (1.1) также покрывается: распаковываем 2 байта в целое беззнаковое число (uint16_t).
    4. ④ — в ③ мы получили длину оставшегося пакета. Очевидно, надо остаток дочитать, что и делаем. Здесь уже не надо отдельно обрабатывать таймаут, достаточно assert. Считанный пакет отдаём в __parsePacket.
    5. ⑤ — если сервер докопался до нас своим пингом, отправим ему понгу.
    6. ⑥ — функция 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,
      }
    }
    1. ① — ждём события internet_ready или stem-client::stop. Так как в event.pullMultiple названия ивентов сверяются через string.match, дефис экранируем. Второй ивент нужен, чтобы принудительно прервать цикл из stop.
    2. ② — обрабатываем мы только internet_ready и только для нашего сокета. Проверяем.
    3. ③ — если поймался пакет или пакеты, то пытаемся обработать каждый в порядке прибытия. Когда мы закончили обрабатывать все пакеты, self:readOne вернёт nil, и цикл прервётся. Кстати говоря, если мы внутри цикла оказались, то соединение установилось. Не забываем отметить это.
    4. ④ — если же улов пуст, перепроверяем, подключены ли мы вообще.
    5. ⑤ — не забываем добавить метод, чтобы остановить наш цикл. Отсюда же отсылаем событие 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,
      },
    }
    1. ① — ensureConnected просто прокинет ошибку, которую вернёт finishConnect().
    2. ② — принимаем защитную позицию против дураков. Рекурсивно запускать циклы смысла нет.
    3. ③ — сначала подключаемся к серверу. Если всё отлично, то можно начинать.
    4. ④ — как и в __run, здесь мы оборачиваем код в цикл. Если вызван stop(), то сначала остановится self.__run, а затем и этот цикл.
    5. ⑤ — обработка исключений требует pcall. Потому что их надо словить.
    6. ⑥ — если мы старались-старались, но так и не смогли уложиться в self.__maxReconnects по реконнектам, кидаемся белым флагом.
    7. ⑦ — не забудем закрыть буфер.
    8. ⑧ — вспомним, что 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!

    ceoMukQ.pngtkFqMPV.png

    Код программы — на гисте.

     

    5. В чём различие между component.internet и require("internet")

    Первое — исходный компонент. Второе — обёртка над ним. У обёртки есть 3 функции:

    • internet.request(url, data, headers) — обёртка над component.internet.request. Удобна тем, что все ошибки превращает в исключения за программиста. Кроме того, возвращаемое значение — итератор, и его можно поместить в цикл for. Тем не менее, код, который ждёт установки соединения, нужно писать самому. К тому же, нельзя задать свой HTTP-метод.
    • 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 механизм лучше.

     

    Поэтому этот пост получился не столько про интернет-карту, сколько про обработку ошибок.


  2. Чтобы отобразить иконки файлов и папок, а затем использовать их как кнопки, нужно разработать удобную в управлении структуру данных.

    При помощи filesystem API можно получить контент текущей директории, что с этим делать?

     

    Для начала разметим экран. В верхней части, на всю ширину экрана будет что-то вроде статус-бара высотой в 4 строки, там будет состояние памяти, батареи, может быть адресная и поисковая строка. Иконки 10x5 символов, с именем снизу, будут располагаться по сетке, через 1 символ.

    xGp7sDC.png

     

    Загруженные иконки уже хранятся в таблице, осталось назначить их файлам и нарисовать.

    При загрузке программы надобно рассчитать, сколько иконок войдет по горизонтали и вертикали, создать таблицу для хранения сетки. Иконка начинает рисоваться от левого верхнего угла, поэтому в таблицу будем заносить именно эти начальные координаты.

    Обзовем таблицу, например, grid.

    В этой же таблице сделаем буфер для хранения имен иконок, чтобы при переходе из папки в папку не рисовать иконки, которые уже есть.

     

    Кстати, все содержимое может не влезть на экран, поэтому будем его разбивать на страницы. Для этого создадим таблицу pages и при сканировании директории будем добавлять в нее таблицы с содержимым страницы, если количество файлов больше размерности #grid.

     

    Сами страницы будут с такими же индексами, что и grid, по индексам будут хранится: имя файла или папки, назначенная иконка и флаг, директория это или нет.

     

    Приступим к описанию функции обновления информации о содержимом.

    Для начала обнулим страницы.

    Получим текущую директорию при помощи filesystem.realPath(os.getenv('PWD')) или shell.getWorkingDirectory().

    Для того, чтобы в результате получить привычный вид, надо будет отсортировать файлы отдельно от папок по алфавиту.

    Для этого создадим две временные таблицы, просканируем директорию через filesystem.list(), если имя оканчивается символом '/', то кидаем его к папкам, иначе к файлам, затем сортируем обе таблицы обычным table.sort().

    Добавляем имена папок к именам файлов в том же порядке, но в начало таблицы и начинаем обработку результата.

    Обходим таблицу с именами файлов, если это папка, то назначаем иконку 'folder', если это ссылка, то 'link', во всех остальных случаях получаем расширение файла паттерном ([^%.]+)$ и пробуем назначить иконку с таким же названием.

    Как-то лень было изучить работу lua-patterns, по идее он должен захватывать одно и больше вхождений, но захватывает от нуля, поэтому файлы с именем расширения, получают иконки.

    Если расширения нет, назначается иконка 'unknown'.

    Далее, в таблицу pages записываем имя файлв, имя иконки и флаг. Потом обновляем индекс, по условию индекс == размерность сетки сбрасываем индекс и обновляем счетчик страниц.


    local W, H = gpu.getResolution() -- получить разрешение экрана
    local grid, pages = {buffer = {}}, {{}} -- создать таблицу для сетки и страниц
    local wm = math.floor(W/11) -- вычислить, сколько иконок войдет по горизонтали
    local index = 1 -- создать счетчик
    for Y = 1, math.floor((H*2-5)/14) do -- пройти цикл по вертикали
      for X = 1, wm do -- пройти цикл по горизонтали
        grid[index] = {x = X*11-9+(W-wm*11-1)/2, y = Y*7-2, z = Y*7+3} -- рассчитать и задать координаты для текущего индекса
        index = index + 1
      end
    end
    
    local function update()
      pages = {{}} -- обнулить страницы
      local index, page, pwd = 1, 1, os.getenv('PWD') -- создать счетчики и получить текущую директорию
      local names, folders = {}, {} -- создать таблицы для имен
      if fs.realPath(pwd) ~= '' then -- если текущая директория не корневая
        folders[1] = '..' -- добавить папку для перехода на верхний уровень
      end
      for name in fs.list(fs.realPath(pwd)) do -- получить имена в текущей папке
        if name:sub(-1) == '/' then -- если в конце слэш
          table.insert(folders, name) -- добавить к папкам
        else -- иначе
          table.insert(names, name) -- к файлам
        end
      end
      table.sort(folders) -- отсортировать имена папок
      table.sort(names) -- отсортировать имена файлов
      for i = #folders, 1, -1 do -- в цикле объеденить имена в одну таблицу
        table.insert(names, 1, folders[i])
      end
      folders = nil -- удалить таблицу для папок
      for n, name in pairs(names) do -- пройти по всем именам
        local icon, isDir -- создать переменные для имени иконки и флага
        if fs.isDirectory(pwd..'/'..name) then -- назначить иконку для папки
          icon, isDir = 'folder', true
        elseif fs.isLink(pwd..'/'..name) then -- назначить для ссылки
          icon = 'link'
        elseif icons[name:match('([^%.]+)$')] then -- если есть иконка для этого расширения
          icon = name:match('([^%.]+)$') -- назначить по имени
        else
          icon = 'unknown' -- для всех остальных назначить стандартную иконку
        end
        pages[page][index] = {name = name:gsub('/', ''), icon = icon, dir = isDir} -- записать имя, имя иконки и флаг в текущую страницу
        if index == #grid then -- если текущая страница заполнена
          index, page = 1, page + 1 -- обновить индекс и номер страницы
          pages[page] = {} -- создать страницу
        else
          index = index + 1 -- обновить индекс
        end
      end
    end

     

    Теперь надо отрисовать иконки по сетке.

    В цикле пройдем по индексам сетки, из координат получим индекс для буфера, для быстрого обращения.

    Если на текущей странице и с текущим индексом что-то есть, а в буфере по этим координатам другая иконка. Берем имя иконки и координаты сетки, вызываем функцию draw_icon(), записываем в буфер имя новой иконки.

    Сбрасываем цвета, стираем зону, где будет имя файла. Пишем имя файла, со смещением, чтобы оно было примерно по центру иконки. Не забывая обрезать имя до 10 символов.

    Если по текущему индексу на странице ничего нет, но в буфере осталось имя иконки. Стираем его из буфера. Устанавливаем фоновый цвет и заливаем иконку вместе с именем по  текущему индексу пустотой.

    local function draw(page)
      page = page or 1 -- если страница не указана, назначить первую
      for index = 1, #grid do -- пройти по индексам сетки
        local hash = grid[index].x*W+grid[index].y -- получить хеш
        if pages[page][index] then -- если на странице по этому индексу есть запись
          if pages[page][index].icon ~= grid.buffer[hash] then -- если новая иконка отличается
            draw_icon(pages[page][index].icon, grid[index].x, grid[index].y) -- нарисовать иконку
            grid.buffer[hash] = pages[page][index].icon -- обновить буфер
          end
          local name = pages[page][index].name
          gpu.setBackground(0) -- задать фоновый цвет
          local color = 0xffffff -- задать цвет текста
          if pages[page][index].dir then -- если это папка
            color = 0xffff00 -- задать другой
          end
          gpu.setForeground(color) -- установить цвет
          gpu.fill(grid[index].x, grid[index].z, 10, 1, ' ') -- очистить место
          gpu.set(grid[index].x+5-#name:sub(1, 10)/2, grid[index].z, name:sub(1, 10)) -- написать имя
        else -- если страница кончилась
          if grid.buffer[hash] then -- если в буфере что-то есть
            grid.buffer[hash] = nil -- обновить буфер
            gpu.setBackground(0) -- задать фоновый цвет
            gpu.fill(grid[index].x, grid[index].y, 10, 6, ' ') -- очистить место
          end
        end
      end
    end

     

    Теперь можно добавить слушателей из части #0, очистить экран, вызвать update() и draw()

    По событию 'click' запускать следующую конструкцию:

    for index = 1, #grid do
      if grid[index].x <= e[3] and grid[index].x+10 >= e[3] and
        grid[index].y <= e[4] and grid[index].y+5 >= e[4] then
        if pages[1][index] then
          if pages[1][index].dir then
            shell.setWorkingDirectory(shell.getWorkingDirectory()..'/'..pages[1][index].name)
            update()
            draw()
            break
          end
        end
      end
    end

     

    Теперь можно ползать по диску.

    a1eZPP8.gif

  3. usgiMAd.png

     

    Иногда случается такое, что ваши компьютеры расположены на расстоянии большем,
    чем стандартные 400 блоков. Wi-Fi отказал, а вам надо связать компьютеры по сети.

     

    Какие тут есть варианты?

     

    1) Повысить лимит в конфиге.
    Это просто, но не всегда возможно.

     

    2) Использовать linked карту.
    С её помощью можно пробить любое расстояние, да.
    Но тут есть несколько своих проблем. Она связывает компьютеры только попарно.
    Для связи нескольких компьютеров надо уже делать сложную систему проброса сообщений.
    Она занимает дополнительный слот. И т.п.

     

    3) Использовать цепочку Wi-Fi карт
    Казалось бы, чем это проще второго варианта?
    А проще оно тем, что тут есть уже готовые библиотеки. =)

     

    Интермедия

     

    Когда-то в стародавние времена, когда трава была зеленее и птички пели громче,
    собралась на нашем сервере компания крутых парней и написала OpenNet.
    Это была полноценная компьютерная сеть. Этакий местный интернет. Она обладала
    своей строгой и надёжной топологией. Каждый узел - своим местом и адресом в сети.
    Никакой анархии. Хочешь - в чатике общайся. Хочешь - сайты строй.

     

    К сожалению, исходники и гайды до сих пор разбросаны по частям по всему форуму.
    Чтобы собрать это снова вместе, потребуется немало усилий.
    Да и зачем нам поднимать полноценный "интернет", если всё, чего мы хотим - это
    пробросить парочку сообщений туда-сюда?

     

    Поэтому в более новые и не такие добрые времена (птички потише, трава потускнее),
    некто Totoro и fingercomp придумали систему попроще и погибче.
    И назвали её Zn.

     

    Приступаем к решению

     

    Итак, как нам связать Васю и Олю, которые живут на противоположных краях Земного Блина?

     

    1) Через микроконтроллёр
    Самое дешёвое в плане ресурсов решение - собрать микроконтроллёр, прошить его
    и закопать где-нибудь в лесу на полпути между Васей и Олей.

     

    Сначала нам потребуется код прошивки. Он идёт в комплекте с Zn библиотекой.
    Как цивилизованные современные люди, мы скачаем её с Hel-репозитория:

    hpm install zn


    Теперь в папке /usr/share/zn/ у нас есть файлик eeprom.lua. Который мы и прошиваем
    на чистый EEPROM:

    flash -q /usr/share/zn/eeprom.lua "Zn node"


    Всё. Осталось вставить чип в контроллер, включить его и закопать.
    Сеть создана!

     


    2) Через компьютер
    Более солидное и основательное решение. Строим в лесу будку. В будке ставим
    компьютер. На компьютер устанавливаем OpenOS и HPM (если он не идёт в комплекте).
    Снова качаем библиотеку:

    hpm install zn


    Создаём мини скрипт:

    edit node.lua


    Пишем в нём такой код:

    (require ('zn')).connect()


    Сохраняем, выходим, запускаем его:

    node


    Всё! Скрипт выполнится и завершится, а в фоновом режиме останется работать
    демон Zn-сети, который будет пробрасывать сообщения.
    По желанию можно сделать скрипт более сложным - например выводить на экран
    сетевой трафик и другую полезную инфу.
    Также можно добавить скрипт в автозапуск компьютера, чтобы даже неожиданные
    сбои питания (белка залезла в трансформатор) не смогли повалить сеть.

     


    Эти два варианта обладают некоторыми недостатком, конечно.
    Чтобы ретранслятор работал автономно, надо ставить чанклодер и источник энергии.
    Однако, в силу своей гибкости, Zn сеть можно организовать и по другому.
    Поднять, так сказать, "лайт версию" ретранслятора.

     

    3) Через планшет
    Устанавливаем на планшет OpenOS и ставим библиотеку и скрипт по методике #2.
    Далее, вручаем планшет соседу Пете и забиваем ему стрелку в том самом лесу.
    На протяжении X минут (где X зависит от терпения Пети) у вас будет полноценная сеть!
    Игрок будет служить чанклодером, а батарея планшета источником питания.

     

    4) Через робота
    Строим робота, устанавливаем скрипт по методике #2. Затем ставим робота где-нибудь
    в незаметном месте (можно спрятать в кроне дерева, так чтобы свет солнца падал
    на солнечную батарею). Всё, мобильный ретранслятор готов.
    Если чанклодеры к роботам разрешены, он может существовать автономно долгое время,
    питаясь солнечной энергией.

     

    5) Через дрон
    Прошиваете EEPROM как для микроконтроллёра, заряжаете в дрона. Дрона запускаете
    в свободный полёт над лесом. Готово! Хотя чанклодер всё равно нужен. Так что вам
    наверное придётся пастись где-то рядом.
    Этот вариант самый сложный, потому что если вы хотите управлять дроном (например,
    с планшета) то вам потребуется модифицировать прошивку и добавить блок управления.
    Зато запустив 1000 дронов, вы можете почувствовать себя Илоном Маском,
    или Цукербергом, раздающим Интернет папуасам.

     

    А как теперь этой сетью пользоваться?

     

    Все просто. Это делается почти как с обычным модемом.
    Только вместо модема вы используете библиотеку Zn.

     

    Если вы знаете адрес модема адресата - можете послать сообщение прямо на него.
    И не важно, через сколько промежуточных узлов оно должно будет пройти. До тех
    пор пока адресат в радиусе досягаемости хотя бы одного узла вашей сети - он ваше
    сообщение получит.

    -- подключаем библиотекуlocal zn = require('zn')-- коннектимся к сетиzn.connect()-- посылаем Оле сообщениеzn.send("адрес модема Оли", "сообщение для Оли")-- при завершении программы не забываем закрыть коннект-- (можно и не закрывать, но зачем тратить ресурсы компа зазря)zn.disconnect()


    Ну а если адресат неизвестен, можно кинуть сообщение бродкастом. Тогда его получат все,
    кто подключен к сети. И адресат, конечно тоже.

    local zn = require('zn')zn.connect()-- посылаем сообщение всем, кто подключён к сети (Оле в том числе)zn.broadcast("сообщение для Оли")zn.disconnect()


    Удачи в построении своих сетей.
    Enjoy Zn!

  4. Портирование мода с 1.14 на 1.7.10 версию оказалось настоящим эпическим сражением, продлившимся целый день. Сейчас я опишу, как мне удалось в нём победить.

     

    Половину дня я пытался понять, почему один мой Gradle (для Minecraft 1.7.10) не может определить версию Java по строке «12.0.1», а другой (для 1.14) заявляет, что надо указать версию плагина «forge». Ключом к решению оказалось использование OpenJDK 10 и обновление Gradle до 4.1 версии.

     

    На этом проблемы не закончились. Gradle стал скачивать нужные файлы, но не смог скачать их все. Некоторые файлы (такие, как twitch-5.16.jar) отсутствуют на двух сайтах-источниках (minecraftforge.net и ещё один), а с официального сайта Minecraft Java скачать файлы не может, так как там есть проблема с сертификатом. Добавление корневого сертификата с minecraft.net в доверенные (тут как раз пригодилось то, что я скачал дополнительную Java) ничего не дало.

    Я скачал все недостающие файлы через браузер, и тут у меня возникла проблема: а в какую папку их надо положить? Не считать ведь хеш-суммы каждого файла и не раскидывать по отдельным папкам руками?

    Решение оказалось очень простым: если Gradle стучится и проверяет, нет ли файла на files.minecraftforge.net, то этот запрос можно перехватить и направить на локальный сервер, где файл уже есть. Как это можно сделать? Правильно, с помощью файла hosts. Тогда я прописал перенаправление с files.minecraftforge.net на 127.0.0.1, поднял локальный сервер и положил туда все нужные файлы. Запустил Gradle - ура, все файлы скачались! Сервер можно выключать.

     

    Дальше осталось совсем немного: посмотреть в исходники каких-нибудь модов под 1.7.10 и разобраться, как они работают. Поменять в своём коде пришлось (почти) только основной класс. Также изменился файл локализации.

     

    В каком виде мод работает (OCTechnics.java):

    Скрытый текст

     

    package org.octechnics.octechnics;

     

    import cpw.mods.fml.common.Mod;

    import cpw.mods.fml.common.Mod.EventHandler;

    import cpw.mods.fml.common.event.FMLPreInitializationEvent;

     

    import cpw.mods.fml.common.registry.GameRegistry;

     

    import net.minecraft.block.Block;

    import net.minecraft.item.Item;

    import net.minecraft.item.ItemBlock;

     

    import org.apache.logging.log4j.LogManager;

    import org.apache.logging.log4j.Logger;

     

    @Mod( modid = "octechnics", version = "1.0" )

    public class OCTechnics {

        public static final Block BASE_FACT = new BasicFactoryBlock("octechnics:basic_factory_block");

        private static Logger logger;

       

        public OCTechnics() {

            logger = LogManager.getLogger();

            logger.info("octechnics - register on EVENT_BUS");

        }

       

        @EventHandler

        public void preInit(FMLPreInitializationEvent evt) {

            logger.info("octechnics - got FMLPreInitializationEvent");

            GameRegistry.registerBlock(BASE_FACT, "basic_factory_block");

        }

    }

     

     

    Файл ./common/blocks/BasicFactoryBlock.java:

    Скрытый текст

     

    package org.octechnics.octechnics;

     

    import net.minecraft.block.Block;

    import net.minecraft.block.material.Material;

    import net.minecraft.creativetab.CreativeTabs;

    import net.minecraft.item.Item;

    import net.minecraft.world.World;

     

     

    public class BasicFactoryBlock extends Block {

        public BasicFactoryBlock(String name) {

            super(Material.rock);

            this.setBlockName(name);

            this.setCreativeTab(CreativeTabs.tabBlock);

            this.setBlockTextureName("octechnics:factory_base");

        }

    }

     

     

    assets/octechnics/lang/en_us.lang:

     

    tile.octechnics:basic_factory_block.name=Factory Base

     

    Текстуры немного переехали: из папки block попали в папку blocks.

     

    Наконец, я запустил мод и он заработал: в первой вкладке творческого инвентаря появился блок с моей текстурой, правильно названный и нормально ставящийся в мир. Этого я и хотел! Дальнейшие приключения (расширение ассортимента и добавление к блокам блок-сущностей) будут в следующей статье.

  5. Xytabich
    Последняя запись

    Дополнительные блоки и предметы.
    Если вы ещё не прочитали вступительную статью - рекомендуется сделать это сейчас, так как описания данных предметов/жидкостей/сущностей здесь нет.

    Про очки и терминал было рассказано в другой статье.

     

    Блоки:

    PIM - Personal Inventory Manager, функционал такой-же как у инвентаря.

    События:
    player_on(name:string, uuid:string) - игрок наступил на платформу
    player_off(name:string, uuid:string) - игрок сошёл с платформы

     

    Сенсор - сканирует область вокруг на наличие блоков или сущностей. Устаревший функционал описывать не буду.
    Радиус сканирования - куб с длиной сторон 2*радиус+1.
    Интересный факт: полученное прокси можно использовать на любом отдалении цели от сенсора, даже если цель в других мирах. Но работает это только до выгрузки чанка с сенсором, или сущности.

    Идентификатор: openperipheral_sensor
    getEntityIds(type:string):number[] - возвращает идентификаторы сущностей заданного типа в радиусе сканирования. Типы: mob, item, minecart, item_frame, painting.
    getEntityData(id:number, type:string):table - возвращает прокси сущности по идентификатору и типу сущности. Позиция сущностей относительно сенсора.
    getPlayers():table[] - возвращает список профилей игроков в радиусе сканирования.
    getPlayerByName(name:string):table - возвращает прокси игрока по никнейму, если он в радиусе сканирования.
    getPlayerByUUID(uuid:string):table - возвращает прокси игрока по уникальному идентификатору, если он в радиусе сканирования.
    sonicScan():table[] - возвращает список блоков в СФЕРИЧЕСКОЙ области!
        - x, y, z:number - координаты блока относительно сканера
        - type:string - тип блока: air, solid, liquid, unknown
        - color:number - битовая маска цвета блока, один из 16ти цветов. 1 << color
    sonicScanTarget(x, y, z:number):table - то-же что и прошлая функция, но для конкретного блока

     

    Селектор - позволяет выбирать предмет на панели.

    Идентификатор: openperipheral_selector
    Событие: slot_click(slot:number, name:string) - адрес компонента не передается, name - похож на адрес, но такого компонента не существует.
    getSlots():table - возвращает список предметов в слотах
    setSlots(items:table) - установить предметы в слоты, не работает в OpenComputers из-за неправильного конвертирования
    getSlot(slot:number):table - получить предмет в слоте
    setSlot(slot:number, item:table) - установить предмет в слот, предмет вида: {id:string, dmg:number}

     
    Билетный автомат - печатает билеты из RailCraft.

    Идентификатор: openperipheral_ticketmachine
    createTicket(destination:string, amount:number):bool - печатает билет(ы) в указанное направление
    getOwner():string - возвращает владельца блока и билетов

     

  6. Мои работы:

     

    + Последний проект - "визуализатор математики на шейдерах":

    https://computercraft.ru/topic/3634-matematicheskiy-vizualizator/

    Создан для исследования математических формул и создания красивых рисунков и анимации. Проект пережил несколько перерождений и с каждым разом сильно менялся в лучшую сторону.

     

    + Игра Атака бактерий-мутантов:
    http://computercraft.ru/topic/1366-igra-ataka-bakteriy-mutantov-na-lua/

    Первый проект на движке love2d, и единственный который я писал не один. Со своим другом. Не зная совершенно синтаксиса lua и понятий массив и функция. (до этого немного игрался вычислениями на vbs)


    + Игра OpenSpace:
    http://computercraft.ru/topic/1525-igra-openspace-kosmicheskiy-emulyator-kosmosa-na-lua/

    Второй проект на движке love2d. Как оказалось, идеально подходит под демонстрацию космической физики и теории относительности для обучения, так как имеет ряд настроек отображения: установление точек отсчёта и отрисовки траекторий по ним, замедление времени и даже гравитацию.

    С технической точки зрения игра была интересна необычным подходом к хранению и обработки системы координат. Чтобы создать по настоящему бесконечный мир игры, пришлось использовать точку отсчёта координат - сам корабль. Что даёт погрешность в вычислениях только на отдалённых от игрока объектах.

     

    + Игра змейка: (с мультиплеером)
    http://computercraft.ru/topic/1419-zmeyka-multipleer-odinochnyy-rezhim/

    Маленькая и простая. Имеет кооперативный режим игры: когда несколько игроков в майнкрайте подходят к блоку компьютера и играют на одном экране. Есть возможность убивать других змеек.

     

    + Игра кликер:
    http://computercraft.ru/topic/1487-openclicker/


    + Игра сапёр:
    http://computercraft.ru/topic/1420-igra-sapyor/


    + Библиотека, для конвертации чисел из одной системы счисления в другую:
    http://computercraft.ru/topic/1387-universalnyy-konverter-sistem-schisleniya-i-tsveta/

    Мне интересна математика и я написал эту библиотеку просто для удовольствия. Хотя позже она мне понадобилась, когда писал скрипт под игру The powder toy

     

    + Моя программа для генерации рисунков (волн) по введённым формулам в консоль (ссылки на код нет, так как программа сырая ещё)

    https://vk.com/album-166016755_261249697

    Написано на движке love2d, на языке lua. Как оказалось, вычисления высокоуровневых языков слишком медленные. Конечно этого хватает на генерацию статичных картинок, но низкий FPS мешает как вводу в консоль формул, так и генерации анимаций в реальном времени.

    Хоть и можно использовать предварительные вычисления, это не решит проблему рендера в реальном времени, например плавного зума картинки.

     

    + Полезная программа на html + js (.hta) для конвертации числа в бинарный код и обратно.

    https://yadi.sk/d/NrgdUa9F3Hyqpt

    Написано для игры The powder toy. В которой в одной переменной типа integer зашифровано несколько значений. Html тут выбран как простой способ отобразить содержимое и наиболее просто реализовать различные элементы ввода. (кнопки, радиокнопки, флажки, списки, поле ввода)

    Синтаксис js изучался впервые. Потому что выбор был использовать js или vbs. А синтаксис vbs я не люблю.


    Мой pastebin:
    https://pastebin.com/u/qwertyMAN_rus

    Мои небольшие скрипты на lua


    Мой YouTube канал:
    https://www.youtube.com/channel/UCU2CFT_PzwbFowZYRsbQVcw/

    Обзоры мною написанных программ и аналитические летсплеи

    Канал создан с целью развития коммуникативных навыков

     

    Мой сборник творчества в ВК:

    https://vk.com/qwertyman_rus

    Содержит ссылки на блоги разработки моих проектов.

     

    Мои эксперименты с графикой здесь: (отсортировано по альбомам)

    https://vk.com/albums-166016755

  7. Дача Игоря

    • 1
      запись
    • 23
      комментария
    • 2017
      просмотров

    Последние записи

    ECS
    Последняя запись

    Еще мой дед говаривал, что каждый кодер на ОС просто обязан начать писать собственный эмуль для самоутверждения. Не желая изменять семейным ценностям, я тоже окунулся с головой в эту клоаку. Вообще в существующих эмуляторах лично меня люто бесит возня с ручной компиляцией, докачиванием всяческих либ по типу openssl, а также отсутствие возможности запуска нескольких виртуальных компиков в едином пространстве с масштабированием экранов, не говоря уже про пересылку данных между ними посредством не менее виртуальных модемов. Поэтому почесав репу, собрав JavaFX + LuaJ, накатав несколько компонентов, на данный момент я заимел следующие зачатки проекта:

     

    • Библиотеки computer, component, unicode
    • Компоненты computer, eeprom, filesystem, gpu, modem, screen, keyboard
    • Имитация системных сигналов по типу touch/drag/drop/key_down/key_up/scroll/modem_message с поддержкой pullSignal/pushSignal
    • Пересылка сетевых пакетов между имеющимися машинами в рабочем пространстве через modem.send/broadcast
    • BSOD для "unrecoverable error"
    • Звуковая система а-ля "комп в мире кубача", имитирующая звуки доступа к диску, и прикольно шумящая на фоне для антуража
    • Создание/сохранение/загрузка виртуальных машин с сериализацией данных имеющихся компонентов. Ну, всяких там адресов, разрешений видях, размеров, координат и т.п.
    • Кнопочка включения (!)

     

    Разумеется, компоненты имеют далеко не все методы, их написание - дело долгосрочное. Но поскольку этот раздел называется блогом, то, кажется, никто не мешает мне писать о запланированном. В идеале хочу замутить компоненты internet, tunnel и data, позволить юзерам выбирать пути к прошивке виртуального EEPROM и содержимому жесткого диска. Также остается открытым вопрос о лимитировании памяти: я понятия не имею, как это реализовать на LuaJ и ублюдочной Яве без обожаемого sizeof(). Городить костыли в виде JavaAgent + Instrumentation.getObjectSize не хочется, но, видимо, придется. Ну, и если у кого-то имеются занятные предложения по функционалу софтины - буду рад.

     

    Сырцы:

    https://github.com/IgorTimofeev/OpenComputersVM

     

    Скриншотик:

     

    7dsXjvM.png

     

     

  8. Права и команды обычного игрока (пояснения по командам будут под списком):

    • Просмотр игровых правил (команда /rules)
    • Просмотр текущего времени (команда /time)
    • Просмотр сообщения дня MOTD (команда /motd)
    • Возврат на предыдущее место,а также на место смерти (команда /back)
    • Телепорт к себе домой (команда /home)
    • Точка телепорт "Дом" устанавливается с помощью кровати (как в обычном minecraft)
    • Доступно одно хранилище размером в 9 ячеек (обновлено 12.02.15)
    • Телепорт на спавн (команда /spawn)
    • Просмотр всех точек телепорта (команда /warp)
    • Телепорт в одну из доступных точек телепорта (команда /warp имяточки)
    • Просмотр и использование системы почты (команды /mail и /mail send)
    • Просмотр и использование системы личных сообщений (команды /msg, /w, /tell /whisper, /t, /m)
    • Использование цвета, форматирования, ссылок, множества получателей в личных сообщениях
    • Быстрый ответ на личное сообщение (команда /r)
    • Написание от 3 лица (команда /me)
    • Игнорирование игрока (его сообщений) (команда /ignore)
    • Получение набора предметов для привата (команда /kit private)
    • Просмотр границ миров (команда /wb list)

    Пояснение по командам:

    Личные сообщения: /msg игрок текст

    Быстрый ответ: /r текст

    Написание от 3 лица: /me текст

    Телепорт на точку телепорта: /warp имяточки

    Система почты: /mail read (чтение), /mail send игрок текст (отправка)

     

    Права и команды хелпера (пояснения по командам будут под списком):

    • Права обычного игрока
    • Получение набора предметов хелпера (команда /kit helper)
    • Возможность ограничить чат игроку (команда /mute)
    • Проверка игрока по IP на мультиаккаунт (команда /ipc)
    • Возможность выкинуть игрока из игры (команда /ipc kick)
    • Просмотр всех действий с блоками в игре (команда /co inspect)
    • Просмотр всех действий игрока (команда /co lookup)
    • Префикс [H]

    Пояснение по командам:

    Ограничение чата: /mute игрок время (время можно указывать в минутах,секундах,часах,днях и т.п. (например 1h 23m заблокирует чат на 1 час и 23 минуты)

    Проверка на мультиаккаунт: /ipc ник игрока

    Выкинуть игрока: /ipc kick ник причина

    Проверка действий с блоками: /co inspect и после чего ударить по блоку (подробнее тут)

    Проверка действий игрока: /co lookup (подробнее тут)

     

    Права и команды программиста:

    • Права обычного игрока
    • Доступ к наборам программистов (команды /kit programmer1 и /kit programmer2)
    • Префикс [P]

    Права и команды модератора:

    • Права хелпера
    • Доступ к набору модератора (команда /kit moderator)
    • Возможность убить игрока (команда /kill ник)
    • Проверка всех игроков онлайн на мультиаккаунт (команда /ipc scan)
    • Возможность забанить игрока (команда /ipc ban ник причина)
    • Возможность телепортироваться к игрокам (команда /tp ник)
    • Возможность телепортировать игрока к игроку (команда /tp кого куда)
    • Возможность вылечить себя (команда /heal)
    • Возможность пополнить полоску голода (команда /feed)
    • Возможность включения режима бога (команда /godmode)
    • Возможность включения режима полета (команда /fly)
    • Префикс [M]

    Права и команды администратора:

    • Все возможные права и команды
    • Префикс [A]

    P.S. если есть дополнения - пишите в комментариях

  9. PieLand

    • 1
      запись
    • 17
      комментариев
    • 3355
      просмотров

    Последние записи

    mrlobaker
    Последняя запись

    Читал я в очередной раз форум, и нашёл тему "Цитадель". Я давно хотел посмотреть, что да как в ОС, поэтому я создал новый мир на своей сборке (в конце кину), подстроил правила под себя, построил бункер... И ушёл писать копалку.
    F9S2dg2.png
    FCKzSfx.png
    0J0cug2.png
    По правилам:

    • Даны компоненты, а не целые компы/дроны/роботы;
    • Внутри бункера можно делать что угодно (даже расширять, но только вниз);
    • Под компом - креативная батарейка из thermal expansion;
    • Энергию от креативной батарейки нельзя никуда подводить!
    • Рядом с единственным зарядником - waypoint для нахождения пути к базе.
    • Цель - сделать систем автоматического создания роботов для добычи большинства ресурсов.


    Я не знаю, на сколько меня хватит, но я надеюсь на хотя-бы 7 записей.
    Следующая запись будет, когда я допишу копалку, а пока всё!

  10. Очумелые ручки

    • 1
      запись
    • 3
      комментария
    • 4858
      просмотров

    Последние записи

    1Ridav
    Последняя запись

    В первой части, которая уже наверное удалена с форума, описывал переделку корпуса флешки, делая её похожей на огрызок кабеля.

     

    В этой части будет немного круче. Было две флешки по 8ГБ на интерфейсе USB 2.0, оба корпуса сломались ввиду того, что были из некачественного пластика даже сами коннекторы. Выкинуть жаба душила, вспомнил, как поделка из первой части хорошо так удивляла тех, кому её давал. Решил сделать нечто похожее.

     

    Для корпуса использован патрон стартера люминесцентных ламп. Обе флешки без корпусов подпаяны параллельно за исключением питания, которое переключается через кнопку.

     

    https://puu.sh/zd11V/a5888e4f1d.jpg
    http://puu.sh/zd0Wf/3de60405ea.jpg

     

    Теперь остаётся собирать лулзы и наслаждаться ступором окружающих :smile3:

  11. LaineBlog

    • 1
      запись
    • 11
      комментариев
    • 8289
      просмотров

    Последние записи

    Итак, сегодня я буду рассказывать как я пишу мод на SAMP. Для начала разберёмся на каком языке пишут скрипты и моды для SAMP. Моды и скрипты в SAMP пишутся на языке PAWN. Pawn - это С-подобный скриптовый язык (как и lua) но, в отличии от lua, в Pawn скрипты именно компилируются,в байт код для запуска на абстрактной машине, а не интерпретируются как в Lua. Скажите - ну и что это даёт? А даёт это многое, например: компилятор pawn ещё до выполнение скрипта проверяет на наличие ошибок, и поэтому у вас никогда не будет внезапных ошибок в программе, также ещё скорость работы скрипта больше чем в том-же Lua, потому-что код скомпилирован в сразу понятный для машины код. Что такое pawn мы разобрались.

     

    Давайте разберёмся с средой разработки, если в lua мы могли писать скрипты хоть в блокноте, то теперь нам нужна полноценная среда разработки.
    1. Pawno - Очень простой редактор, в есть необходимый минимум чтобы писать скрипты на pawn.
    blogentry-0-0-77889400-1489396166_thumb.png


    Плюсы:
    + Малый размер (768 кб)
    + Идёт сразу с Samp server
    + Встроенный список функций из всех инклудов
    + Сразу есть все паблики и функции samp
    Минусы:
    - Подсветка синтаксиса сделана чисто для галочки (всего два цвета подсветки синий и чёрный)
    - На windows начиная с vista надо запускать от имени админа
    - На больших скриптах может вылетать

     

     

    2. Notepad++ (с плагином nppexec) - Самый популярный редактор скриптов. Поддерживает плагины, и также множество языков программирования
    blogentry-0-0-87342400-1489396159_thumb.png


    Плюсы:
    + Расширяемость
    + Нормальная подсветка синтаксиса
    + Авто-табуляция кода
    + Удобная навигация по коду (можно сразу перейти к другой строке, и есть карта документа)
    Минусы:
    - Для поддержки pawn надо много чего настраивать.
    - Нету Встроенного списока функций
    Настройка плагина NppExec:
    1. Выберите plugin manager
    blogentry-0-0-71315900-1489396173_thumb.png
    2. Откроется окно, ищём Nppexec, выбираем галочкой, жмём install, перезапускаем.
    blogentry-0-0-41688900-1489396176_thumb.png
    3. Должно появится в меню пункт, жмём
    blogentry-0-0-59483000-1489396174_thumb.png
    4. Откроется окно
    blogentry-0-0-46710500-1489396175_thumb.png
    вписываем туда код:

    cd $(CURRENT_DIRECTORY) "Путь до pawncc.exe" "$(FILE_NAME)" -; -(


    5. Нажимаем ok и компиляция начнётся


    Но давайте перейдём к написанию программы "hello world!" Как я и сказал у меня samp вариант Pawn. Вот как выглядит hello world в pawn:

    main(){	print("hello world!");}


    Компилируем:
    blogentry-18530-0-49339800-1489396885_thumb.png
    Как видим, всё прошло успешно и компиляция завершена.

     


    Вот как выглядела бы ошибка:
    blogentry-18530-0-22886300-1489396999_thumb.png
    С компиляцией разобрались, теперь нам надо запустить сервер, запускаем сервер и видем наше сообщение:
    blogentry-18530-0-32421200-1489397235_thumb.png
    Теперь хотелось-бы чтобы например: hello world писалось не в консоль сервра, а например игроку в чат. Для этого нужно использовать include, да-да как и в си или c++ pawn поддерживает include и константы #define, и даже команды пре-процесса #pragma. Теперь, давайте подключим include к нашему скрипту для того, чтобы подключить include надо в начале скрипта написать #include <a_samp>, тем самым мы подключили include для работы функций samp. Теперь мы можем создать код в нашем скрипте:

    public OnPlayerConnect(playerid) // Создаём паблик чтобы при подключении игрока что-то происходило{	SendClientMessage(playerid, -1, "hello world"); // Функция отправки сообщения        return 1; // функция должна что-то возвращать}


    Playerid - Ид игрока которому мы будем отправлять сообщение (в данном случае игроку который подключился к серверу)
    -1 - Цвет сообщения (белый)
    "hello world" - Строка которая будет отправляться.
    Запускаем сервер, заходим в игру и видим наше сообщение:
    blogentry-18530-0-26346100-1489398324_thumb.png
    Ну вот и всё это был весь мой обзор языка pawn. ВНИМАНИЕ! Я некого не собираюсь учить (я сам учусь) это был просто мини-обзор языка Pawn. Потому-что никто на форуме не знает этот замечательный язык программирования. Если бы он был в OpenComputers я бы был рад!

  12. Programist135 Soft

    Programist135
    Последняя запись

    Всем привет!!!

     

    Это моя уже третяя программа в моём магазине приложенийблоге. Для неё понадобится уже робот.
    Итак, приступим.

     

    1. Комплектация и сборка
    Вам понадобится:

    • Системный блок 2 уровня
    • Геолайзер
    • Интернет-карта
    • Видеокарта 1 уровня
    • Монитор 1 уровня
    • Клавиатура
    • Дисковод
    • EEPROM c Lua BIOS
    • Дискета с OpenOS


    Собираем нашего робота. После чего вставляем в него дискету и устанавливаем OpenOS.

     

    2. Поле
    Строим следующее:
    UbZZDN6.png
    И ставим рядом с роботом зарядник и подводим к нему редстоун-сигнал и питание.

     

    3. Запуск
    Пишем в роботе следующее:
    pastebin get pV2iGZ2n /farm.lua
    А дальше набираем farm и.. Готово! Ваш робот "прочешет" всю ферму, если найдёт выросшую пшеницу (metadata = 7) то он её срубит и посадит снова. А ещё в программе ведутся логи с достаточно высоким приоритетом. Логируется даже инфа о каждой пшенице.
    c8V3Ut0.png4. В следующей версии
    В следующей версии наверное будет следующее:

    • Воздействие костной мукой
    • Проверка вспаханности земли


    Ну вот и всё, надеюсь вам эта программа пригодится, всем пока!

  13. XK893iv.pngRust


    Сегодня познакомимся с функциями и указателями, а также по мелочи: зоны видимости.

     

    Функции
    Функции в расте похожи на функции в Си, Jawa, луа. Для создания функции используется ключевое слово fn.

    fn add(a: i32, b: usize) -> usize {  a + b}


    После ключевого слова fn идет имя функции, далее в скобках указываются аргументы через запятую, и опционально, стрелочка -> и возвращаемый тип.
    Указывая аргументы нужно указывать тип аргумента после символа :.

     

    Функции в которых не указано возвращаемое значение, возвращают ()

     

    Ключевое слово return возвращает значение из функции.

    fn answer() -> i32 {  return 42;}


    return не всегда обязателен, позже разберемся где он нужен, а где не нужен.

     

    Указатели
    Указатели в расте гораздо безопаснее чем указатели в Си. Создать указатель на неизменяемые данные можно при помощи &.

    let a = 10;let pa = &a;


    Получить значение из указателя можно при помощи символа *.

    let a = 10;let pa = &a;println!("a = {}; *pa = {}; a + a = {}; *pa + *pa = {}", a, *pa, a + a, *pa + *pa);// \--> a = 10; *pa = 10; a + a = 20; *pa + *pa = 20


    Но в некоторых случаях писать * не обязательно.

    let a = 10;let pa = &a;println!("a = {}; pa = {}; a + a = {}; pa + pa = {}", a, pa, a + a, pa + pa);// \--> a = 10; pa = 10; a + a = 20; pa + pa = 20


    Раст сам все поймет, за что ему спасибо.
    Иногда мы хотим создать изменяемую ссылку. Для этого используется &mut.

    let mut a = 10;let pa = &mut a;*pa += 1;   // здесь * обязателен    println!("{}", pa);   //-->  11


    Если мы хотим изменить значение на которое указывает наш указатель, * обязателен. Без него раст не может понять, мы хотим присвоить новый указатель, или значение.
    В расте используется концепция "либо один писатель, либо много читателей". В примере выше, переменную a прочитать мы уже не сможем, и писать в нее тоже не сможем, так как она передана указателю pa. pa – писатель.

     

    Бывают случаи когда мы хотим присвоить указателю новое значение, то есть сделать так, что бы указатель указывал на другие данные. Это тоже предусмотрено.

    let mut a = 10;let mut b = 20;let mut pa = &mut a;    println!("{}", pa);  // --> 10    pa = &mut b;    println!("{}", pa);  // --> 20    println!("{}", a);  // --> 10//             ^  ошибка, a уже имеет писателя, читателя не создать.    println!("{}", &a);  // --> 10//             ^-  ошибка, a уже имеет писателя, читателя не создать.    println!("{}", &mut a);  // --> 10//             ^-----  ошибка, a уже имеет писателя, еще одного не создать.


    Как видите, все правила про "либо один писатель, либо много читателей" работают даже если на a уже не указывает ничего.

     

    Зоны видимости
    Зоны видимости в расте ничем не отличаются от тех же в Луа.

    fn main() {  let a = 10;  {    let b = 20;    println!("{}", a);   // --> 10    println!("{}", b);   // --> 10  }  println!("{}", a);   // --> 10  println!("{}", b);   // --> 10  //             ^  ошибка, b нет.}


    Хочу отметить только, что в расте все переменные удаляются когда уйдут из зоны видимости. Исключением являются только значения которые возвращаются в функциях.

     

    Хозяйке на заметку:

    {
    и
    )
    это аналог
    do
    и
    end
    в Луа.

     

    На сегодня все. Извините что запоздал с 3 частью, так уж вышло. В следующий раз познакомимся с перечеслениями (enum) и структурами (struct). =)

  14. eu_tomat
    Последняя запись

    Здравствуй, брат автоматизатор!

     

    Желаешь ли ты услышать от меня несколько мыслей о красивом, понятном, и эффективном коде?
    На истину в последней инстанции я не претендую, и ты, возможно, знаешь решения получше моих. В этом случае оставь свои соображения в комментариях, и код на проекте станет немного чище.

     

    Небольшое отступление:
    Полгода назад я задумал написать серию статей о программировании для новичков, где я писал код и объяснял, почему он должен писаться так, а не иначе. Но такое объяснение оказалось для меня непосильным занятием, т. к. мне приходилось спорить с самим собой. Легко было только один раз, когда я обнаружил серьезный дефект, наверное, во всех копалках на проекте. Но потом я получил стрелу в колено...
    Вернувшись на проект, я неожиданно для себя понял, что демонстрировать эффективность удачного кода значительно проще, противопоставляя его коду неудачному.

     

    В прошлый раз я рассказал о том, как математика помогает писать более простые, понятные и при этом более эффективные программы.
    qwertyMAN заметил, что использование взятия остатка от деления – это не математическая проблема, и я с ним отчасти согласился. Решение о том, использовать или нет операцию %, мало влияет на общий алгоритм, но сильно влияет на то, как будет выглядеть готовый код. Но всё-таки об этой операции следует помнить уже на этапе проектирования алгоритма, а не только на этапе кодинга.
    А вот, о тонкостях написания кода я хочу рассказать сегодня, не особо вдаваясь в общие алгоритмы. Моим подопытным будет уже знакомый код из охранной системы турелей.
    Вот сам код:


    local function func()	local com					= require("component")	local event					= require("event")	local gpu					= com.gpu	local turret				= com.os_energyturret	local radar					= com.openperipheral_sensor	local autors				= {"qwertyMAN"}	local version				= "0.9.1"	-- Настройки	local firePleayers			= true	local fireMobs				= true	local Black_Player_List		= {}	local White_Player_List		= {}	-- относительные координаты пушки от сканера	local correct = {		x = 0,		y = 4,		z = 0	}	local function pointer(x,y,z)		local distXY = math.sqrt(x^2+z^2)		local distDY = math.sqrt(y^2+distXY^2)		local outX = math.deg(math.acos(x/distXY))+90		local outY = 90-math.deg(math.acos(y/distDY))		if z<0 then			outX = (180-outX)%360		end		return outX,outY	end								while true do		os.sleep(0)		local target = false		local fire = true		local scan=radar.getPlayers()		if firePleayers and #scan>0 then			if #White_Player_List>0 then				for i=1, #autors do					White_Player_List[#White_Player_List+1] = autors[i]				end				for i=1, #scan do					local swich = true					for j=1, #White_Player_List do						if scan[i].name==White_Player_List[j] or not scan[i].name then							swich = false						end					end					if swich then						target = scan[i].name						break					end				end			elseif #Black_Player_List>0 then				for i=#Black_Player_List, 1, -1 do					for j=1, #autors do						if Black_Player_List[i] == autors[j] then							table.remove(Black_Player_List,i)						end					end				end				for i=1, #scan do					local swich = false					for j=1, #Black_Player_List do						if scan[i].name==Black_Player_List[j] then							swich = true						end					end					if swich then						target = scan[i].name						break					end				end			else				if #autors>0 then					for i=1, #autors do						White_Player_List[#White_Player_List+1] = autors[i]					end				else					target = scan[1].name				end			end			if target and radar.getPlayerByName(target) then				target=radar.getPlayerByName(target).all()				local x,y,z = target.position.x-0.5-correct.x, target.position.y+0.3-correct.y, target.position.z-0.5-correct.z				local vx,vy = pointer(x,y,z)				turret.moveTo(vx,vy)				if turret.isOnTarget() then					turret.fire()				end				fire = false			end		end		target = false		if fireMobs and fire then			local scan=radar.getMobIds()			if #scan>0 then				for i=1, #scan do					local mob					if radar.getMobData(scan[i]).basic() then						mob = radar.getMobData(scan[i]).basic()						target = mob					end				end				if target then					local x,y,z = target.position.x-0.5-correct.x, target.position.y-0.5-correct.y, target.position.z-0.5-correct.z					local vx,vy = pointer(x,y,z)					turret.moveTo(vx,vy)					if turret.isOnTarget() then						turret.fire()					end				end			end		end	endend-- мегакостыльwhile true do	local oop = pcall(func)	print(oop)end

     

     

    Первое, на что я обратил внимание – это использование pcall. Именно оно и побудило меня к разбору кода, но прежде я предлагаю обсудить другой нюанс.

     

    1. Избегай повторения уже выполненных вычислений
    Имеются два фрагмента кода:

    if target and radar.getPlayerByName(target) then  target=radar.getPlayerByName(target).all()...local mobif radar.getMobData(scan[i]).basic() then  mob = radar.getMobData(scan[i]).basic()  target = mob

    И в обоих фрагментах происходит дублирование вызовов. Первый раз вроде как для проверки. Но такой код всё равно не снимает проблему, т. к. при первом вызове функция может вернуть одно значение, а при втором вернет другое. Правильным решением будет сохранить результат в переменную. И если ее значение удовлетворяет условиям, использовать ее в дальнейшем. Во втором фрагменте следует заодно избавиться от переменной mob, которая больше нигде не используется.

    if target then  target = radar.getPlayerByName(target)  if target then    target=target.all()...target = radar.getMobData(scan[i])if target then  target = target.basic()

    Почему я проверяю результаты getPlayerByName и getMobData, а не других функций вроде all() или basic()? Потому что именно они могут вызвать ошибку. Ошибка возникает от того, что игрок или моб может покинуть зону действия сенсора, пока выполняется обработка внутри программы. К сожалению, указанной проверки недостаточно, т. к. мод OpenPeripheral вместо того чтобы вернуть nil или иным образом указать на проблему, тупо генерирует ошибку, не оставляя иного варианта кроме использования pcall.

     

    2. Используй защищенный режим только там, где это необходимо
    Код программы выглядит примерно так:

    local function func()  -- почти весь код программы помещен в эту функцию  -- включая инициализацию переменных и определение функций  ...  -- этот бесконечный цикл прерывается из-за отсутствующей обработки ошибок  while true do    ...    local target = false    local scan=radar.getPlayers()    ...    target = ...    if target and radar.getPlayerByName(target) then      target=radar.getPlayerByName(target).all()      ...    end    ...  endend-- мегакостыль (комментарий самого автора)while true do	local oop = pcall(func)	print(oop)end

    Автор понимает, что использует костыль, но правильное решение использовать не хочет. Подобные городушки провоцируют появление новых: уже сейчас на ровном месте появилась дополнительная функция и вложение двух бесконечных циклов. Но главная проблема этого кода в том, что pcall скрывает любые ошибки, и дальнейшая отладка программы становится затруднительной. Даже печать результата, возвращаемого pcall, реализована неверно – ничего кроме false выведено не будет.
    Для решения проблемы следует использовать pcall точечно, только там, где это необходимо, а именно в вызове getPlayerByName и getMobData. А если есть возможность вообще обойтись без pcall, то в готовой программе без него следует обойтись. В нашем случае обойтись без pcall, похоже, нельзя. Поэтому убираем наш костыль и функцию func, оставив лишь ее содержимое, и дорабатываем проблемные участки таким образом:

    if target then  local flag,res = pcall(radar.getPlayerByName,target)  if flag then    target=res.all()...local flag,res = pcall(radar.getMobData,scan[i])if flag then  target = res.basic()

    Пришлось ввести дополнительные переменные, зато код избавился от костыля со всей его обвязкой, а ошибка, генерируемая модом OpenPeripheral, локализована, и новых проблем не создаёт. Программа работает стабильно и ее отладка не нарушена. На этом можно было бы и закончить, но я уже вошёл во вкус. Поэтому продолжу давать советы, иллюстрируя их фрагментами кода:

     

    3. Выноси за цикл всё, что возможно
    Это правило подобно первому, но пренебрежение им не так бросается в глаза начинающего программиста, т.к. сам код не дублируется, зато дублируется его исполнение.
    Восстановим общий алгоритм работы программы, чуть подробнее разобрав этот кусок:

    while true do  local target = false  ...  local scan=radar.getPlayers()  if firePleayers and #scan>0 then    if #White_Player_List>0 then      добавление списка авторов в в белый список      target = первый игрок на радаре вне белого списка    elseif #Black_Player_List>0 then      удаление авторов из черного списка      target = первый игрок на радаре в черном списке    else      if #autors>0 then        перенос авторов в белый список      else        target = первый игрок на радаре  вычисление координат цели и выполнение выстрела  сканирование мобов, вычисление их координат с последующим расстрелом.

    Как ты уже догадался по заголовку, в бесконечном цикле выполняется что-то явно лишнее. А именно, работа со списком авторов. Всё это могло бы прекрасно работать и до основного цикла, создавая при этом нужный эффект. Более того, вынос этого кода за цикл позволит исправить серьезнейшую ошибку: на каждой итерации бесконечного цикла происходит добавление авторов в белый список, при этом их наличие в белом списке никак не проверяется, но каждый раз происходит добавление новых. Даже с двумя планками памяти 3.5 уровня через сотню-другую тысяч итераций программа завершится с ошибкой «not enough memory».
    Вынося лишние действия за цикл, ты избавишь программу как от неконтролируемого расхода памяти, так и от лишних действий, замедляющих ее работу. Думаю, демонстрировать корректный код здесь излишне. Достаточно лишь вынести эти участки кода из цикла.

     

    В программе присутствуют и менее очевидные вычисления, которые можно вынести за цикл.

    -- этот код находится за цикломlocal correct = { x = 0, y = 4, z = 0 }...-- а этот внутриlocal x,y,z = target.position.x-0.5-correct.x, target.position.y+0.3-correct.y, target.position.z-0.5-correct.z...local x,y,z = target.position.x-0.5-correct.x, target.position.y-0.5-correct.y, target.position.z-0.5-correct.z

    Логичным будет переписать код таким образом:

    -- этот код за цикломlocal correct = { x = 0+0.5, y = 4+1, z = 0+0.5 }...-- а этот внутриlocal x,y,z = target.position.x-correct.x, target.position.y+1.3-correct.y, target.position.z-correct.z...local x,y,z = target.position.x-correct.x, target.position.y+0.5-correct.y, target.position.z-correct.z

    Так мы избавимся от лишних сложений и вычитаний в цикле. Лишние сложения при инициализации таблицы correct не сильно помещают, т. к. они выполняются всего один раз, а нужны они для наглядности кода. Поясню, что здесь происходит.
    Во-первых, турель и сенсор находятся в разных блоках, поэтому координаты игрока следует скорректировать на эту разницу. Координаты турели относительно сенсора хранятся в таблице correct.
    Во-вторых, сенсор определяет координаты игроков и мобов относительно своего северо-западного угла, а турель вращается вокруг центра блока. Поэтому приходится корректировать координаты (x,z) на половину блока по горизонтали.
    В-третьих, сама турель стреляет на один блок выше уровня ног игрока или моба. Автор корректирует высоту цели в зависимости от цели: для игрока прицел приподнимается на 0.3 блока, чтобы игрок не мог уходить от выстрела прыжком или прятаться за блоки, а для мобов прицел опускается на полблока, чтобы попадать, например, в кур или свиней. Но тут тоже не всё просто. Чтобы попадать в цыплят, прицел следует опустить еще ниже, но тогда турель промахивается мимо взрослых кур, стреляя им куда-то под ноги. Для эффективной стрельбы по любым мобам нужен алгоритм, определяющий вид моба, его возраст, и находящий по таблице его рост. Причем, нужен не только рост. Например, нет смысла целиться в нижнюю часть слизня, т. к. тот постоянно прыгает. В общем, это отдельная тема для поиска оптимального алгоритма, сейчас же я хочу продолжить рассказ о кодинге.
    В итоговом коде не только вынесены лишние вычисления из цикла. Кроме этого он стал более логичным: числа 1.3 и 0.5 по сути означают высоту цели относительно ног игрока или моба.

     

    4. Прерывай циклы, когда они уже выполнили свою задачу
    Вот два подобных друг другу фрагмента кода:

    local swich = truefor j=1, #White_Player_List do  if scan[i].name==White_Player_List[j] or not scan[i].name then    swich = false  endendif swich then...local swich = falsefor j=1, #Black_Player_List do  if scan[i].name==Black_Player_List[j] then    swich = true  endendif swich then

    Задача циклов в том, чтобы при подходящем случае изменить значение переменной switch. Но как только оно изменилось, зачем продолжать работу? Далай break. Иначе выполнение твоей программы замедляется.
    Пока я писал этот текст, то напрягался при каждом наборе названия переменной swich. Нет такого слова, зато есть слово switch, и не глядя я набирал именно его. Поэтому буду писать switch. Кроме того, название переменной все равно не отражает ее сути. С тем же успехом можно было использовать однобуквенное название переменной. А лучше бы и вовсе избавиться от нее.

     

    5. Избавляйся от лишних переменных
    Вот те же фрагменты в немного дополненном составе, внутри других циклов и с оператором break, как же теперь без него:

    local target = false...for i=1, #scan do  local swich = true  for j=1, #White_Player_List do    if scan[i].name==White_Player_List[j] or not scan[i].name then      swich = false      break    end  end  if swich then    target = scan[i].name    break  endend...for i=1, #scan do  local swich = false  for j=1, #Black_Player_List do    if scan[i].name==Black_Player_List[j] then      swich = true      break    end  end  if swich then    target = scan[i].name    break  endend

    Что выполняют оба фрагмента? Ищут подходящую цель по белому и черному спискам игроков.
    Где хранится цель? В переменной target.
    Что хранится в переменной switch? Флаг того, что переменная target должна быть изменена.
    А зачем нам этот флаг? Что мешает сразу изменить переменную?

    local target = false...for i=1, #scan do  target = scan[i].name  for j=1, #White_Player_List do    if scan[i].name==White_Player_List[j] or not scan[i].name then      target = false      break    end  end  if target then    break  endend...for i=1, #scan do  for j=1, #Black_Player_List do    if scan[i].name==Black_Player_List[j] then      target = scan[i].name      break    end  end  if target then    break  endend

    Код стал немного короче и быстрее, но и это еще не предел.

     

    Еще непонятно, что делает, or not scan.name в условии. Повлиять это выражение может в том случае, если scan.name будет равно false, или nil. А разве такое возможно? Даже не знаю, как классифицировать этот недочет. Видимо, от старых экспериментов осталось. Посоветовать можно только одно: вычищать код перед публикацией.

     

    6. Используй ассоциативные возможности таблиц Lua
    Таблицы в Lua – это больше чем массивы. Это и объекты, и списки, и словари. А может, и что-то еще, о чем я забыл или даже не знал.
    Таблицы в Lua – это наборы пар ключ-значение. В качестве ключей и значений таблицы может быть что угодно кроме nil.
    Пара ключ-значение в таблицах Lua присутствует даже если мы не используем ключи явным образом.
    Попробуй запустить такой код:

    PlayerList={"Ded", "Baba", "KurochkaRyaba"}for k,v in pairs(PlayerList)do print(k,v)endfor i=1,#PlayerList do print(PlayerList[i])endPlayerList={[1]="Ded", [2]="Baba", [3]="KurochkaRyaba"}for k,v in pairs(PlayerList)do print(k,v)endfor i=1,#PlayerList do print(PlayerList[i])endPlayerList={["Ded"]=1, ["Baba"]=2, ["KurochkaRyaba"]=3}for k,v in pairs(PlayerList)do print(k,v)endfor i=1,#PlayerList do print(PlayerList[i])endprint(#PlayerList)print(PlayerList["Ded"])print(PlayerList["Baba"])print(PlayerList["RedHatBaby"])

    blogentry-13296-0-54921600-1459108363_thumb.png
    В первом случае мы не указываем ключи явно, но они создаются. Мы свободно перебираем как пары ключ-значение, так и значения по их индексу, который совпадает с ключом.
    Во втором случае мы явно указали ключи. Перебор пар ключ-значение показывает, что элементы таблицы хранятся в неведомом нам порядке, скорее всего, в соответствии с неким хешем, но на перебор по индексу это никак не влияет, порядок не нарушен, а это самое главное.
    В третьем случае мы поменяли ключи и значения местами. Естественно, ни о каком переборе по индексу теперь не идет и речи. Более того, определить длину таблицы теперь тоже не так просто. Зато появилась возможность, не выполняя перебор всех значений в цикле, одной командой определить, присутствует ли игрок в списке. Для игроков Дед и Баба есть значение, а игрок КраснаяШапочка в список не внесен, и имеет значение nil.

     

    Как это соотносится с нашим кодом? Попробуем заполнять списки игроков таким образом:
    local Black_Player_List = { ["qwertyMAN"]=1 }
    local White_Player_List = { ["Ded"]=1, ["Baba"]=1, ["KurochkaRyaba"]=1 }
    В качестве значения я использовал 1, и оно может быть любым, но 1 - записывается кратко. Главное, чтобы не nil. И не false, чтобы проще было выполнять проверку элемента.
    То, что qwertyMAN оказался в черном списке, ему никак не повредит, он же автор.

     

    Теперь все фрагменты кода изменятся таким образом:

     

    Добавление списка авторов в в белый список:

    -- былоfor i=1, #autors do  White_Player_List[#White_Player_List+1] = autors[i]end-- сталоfor i=1, #autors do  White_Player_List[autors[i]] = 1end

    Код немного укоротился, а главное – теперь переполнение памяти не грозит даже при работе в бесконечном цикле, т. к. элемент создается один раз, а в последующие – лишь изменяется его значение.

     

    Удаление авторов из черного списка:

    -- былоfor i=#Black_Player_List, 1, -1 do  for j=1, #autors do    if Black_Player_List[i] == autors[j] then      table.remove(Black_Player_List,i)    end  endend-- сталоfor j=1, #autors do  Black_Player_List[autors[i]] = nilend

    Код заметно укоротился.
    Сомневаешься, действительно ли элемент таблицы удаляется? Запусти этот код, и все станет понятным:

    PlayerList={["Ded"]=1, ["Baba"]=2, ["KurochkaRyaba"]=3}for k,v in pairs(PlayerList)do print(k,v)endPlayerList["Ded"]=nilfor k,v in pairs(PlayerList)do print(k,v)end

    И напоследок два уже разобранных перед этим фрагмента, которые еще более упростились:

    -- былоfor i=1, #scan do  target = scan[i].name  for j=1, #White_Player_List do    if scan[i].name==White_Player_List[j] then      target = false      break    end  end  if target then    break  endend...for i=1, #scan do  for j=1, #Black_Player_List do    if scan[i].name==Black_Player_List[j] then      target = scan[i].name      break    end  end  if target then    break  endend-- сталоfor i=1, #scan do  if not White_Player_List[scan[i].name] then     target = scan[i].name     break  endend...for i=1, #scan do  if Black_Player_List[scan[i].name] then      target = scan[i].name      break    endend

    7. Минимизируй идентичные участки кода
    При добавлении сходного функционала в программу можно скопировать рабочий участок кода и немного изменить его. Иногда до неузнаваемости. Но если фрагмент достаточно велик, а изменения малы, то правильнее будет вынести этот фрагмент в отдельную функцию. Во-первых, итоговой код будет более компактным. Во-вторых, при необходимости будет проще вносить изменения, не правя все участки кода.
    В программе имеется два таких похожих участка:

    local x,y,z = target.position.x-correct.x, target.position.y+1.3-correct.y, target.position.z-correct.zlocal vx,vy = pointer(x,y,z)turret.moveTo(vx,vy)if turret.isOnTarget() then  turret.fire()end...local x,y,z = target.position.x-correct.x, target.position.y+0.5-correct.y, target.position.z-correct.zlocal vx,vy = pointer(x,y,z)turret.moveTo(vx,vy)if turret.isOnTarget() then  turret.fire()end

    Ранее мы уже выяснили, что единственная изменяемая величина здесь – это коррекция высоты цели. Сейчас основной вопрос: что из этого следует вынести в отдельную функцию. С точки зрения минимизации кода следует выносить почти всё. Но чтобы сделать код более логичным, не следует всё мешать в одну кучу. Лучшим решением мне кажется вынос всего, что связано с вычислениями, в уже имеющуюся функцию pointer. Вот она:

    local function pointer(x,y,z)  local distXY = math.sqrt(x^2+z^2)  local distDY = math.sqrt(y^2+distXY^2)  local outX = math.deg(math.acos(x/distXY))+90  local outY = 90-math.deg(math.acos(y/distDY))  if z<0 then    outX = (180-outX)%360  end  return outX,outYend

    Учитывая то, как я переписал эту функцию в прошлый раз, а также избавляясь от лишних переменных и перенося часть вычислений внутрь функции, перепишу код таким образом:

    local function pointer(pos,h)  local x,y,z = pos.x-correct.x, pos.y-correct.y+h, pos.z-correct.z  local azimuth=math.deg(math.atan2(x,-z))%360  local elevation=math.deg(math.atan2(y,math.sqrt(x*x+z*z)))  return azimuth, elevationend...turret.moveTo(pointer(target.position,1.3))if turret.isOnTarget() then  turret.fire()end...turret.moveTo(pointer(target.position,0.5))if turret.isOnTarget() then  turret.fire()end

    Конечно, этот код можно ужать еще плотнее, но я увижу в этом смысл, если дальнейшая доработка программы вынудит меня продублировать эти блоки кода.

     

    На этом закончу. Возможно, я дал бы тебе еще пару советов, но за исключением описанных выше моментов код qwertyMAN'а вполне адекватен.

     

    Программируй красиво, брат!

  15. Блог недоблоггера

    • 2
      записи
    • 15
      комментариев
    • 8462
      просмотра

    Последние записи

    Доброго времени суток.
    Не секрет наверное, что UU-валюту можно получить, проголосовав за проект на рейтинговых порталах (мониторингах серверов MC).
    Но бывает так, что слишком сильно засиживаешься в игре, что забываешь проголосовать и теряешь в день целых 70 UU.
    Я по началу тоже пропускал (забывал) голосовать. И я решил, а что если написать "напоминалку" о том, что нужно проголосовать.

     

    Итак, данная статья рассчитана на linux-пользователей (для windows-юзеров я ниже напишу альтернативу этому).

     

    Что нужно для того, чтобы сделать "напоминалку":

    • Любой linux-диструбутив (у меня Arch Linux)
    • Любое DE (рабочая среда. Пример у меня KDE5)
    • bash (с этим проблем нет у линуксеров)
    • Установленный пакет zenity (утилита, которая позволяет выводить на экран диалоговые окна GTK+ из командной строки и скриптов командной оболочки. С установкой в ubuntu и linux mint с этим проблем нет. Для других дистрибутивов надо копать самим этот пакет. В Arch Linux он есть)
    • cron (он же планировщик задач. По-умолчанию его нет в десктопных версиях дистрибутивов. Нужно поставить самим)
    • Руки, растущие из плеч (ибо результат может отличаться от примера в статье)


    Я долго не стану расписывать что есть что. Документацию по zenity и cron можно найти спокойно в сети.
    Я лишь сразу выложу уже готовый bash-скрипт, который выводит данное окошечко на экран:

     

     

     

     

     

    dzehCAN.png


    Вот собственно сам код:

    #!/bin/bashDISPLAY=:0.0 /usr/bin/zenity --info --no-wrap --title="Время голосовать" --text="<i>Настало время проголосовать за проект</i> <a href='http://computercraft.ru'><b>computercraft.ru</b></a>\n\n<a href='http://mcrate.su/rate/5123'><b>mcrate.su</b></a> | <a href='http://topcraft.ru/servers/3000'><b>topcraft.ru</b></a> | <a href='http://monitoringminecraft.ru/top/computercraft'><b>monitoringminecraft.ru</b></a>" --ok-label="Уже спешу голосовать"


    И чтобы это окошко вылезало каждый день в определенное время напишем в cron задачу (по-умолчанию добавление задачи осуществляется командой в консоле crontab -e. Ну это так для тех кто забыл):

    00 22 * * *     /bin/bash /home/user/vote.sh


    Коротко скажу что делает данная задача.
    00 22 - это время. Я поставил 22:00 (моего времени местного стоит отметить).
    * * * - это день, месяц, день недели (нам надо чтобы каждый день выполнялось поэтому ставим *).
    /bin/bash /home/user/vote.sh - собственно сама команда на исполнение скрипта. Я всегда прописываю полный путь до bash, чтобы не было проблем, а далее указываем путь, где лежит файл-скрипт.

     


    В общем-то и все. "Напоминалка" готова.
    Ах да, забыл сказать, после добавления и сохранения в crontab задачи не забудьте перезапустить демон (для тех у кого стоит init.d, а это в основном убунтеры и линукс минтеры для них sudo service cron restart, для systemd - sudo systemctl restart cronie.service (я поставил пакет cronie)).

     

    Не забывайте голосовать за проект и удачи в программировании :smile9:

     

    P.S. Как и обещал для windows-пользователей. Можно конечно поставить zenity (И если хватит скилла настроить через стандартный планировщик задач Windows вызов bat-скрипта с настроенным path до zenity, то респект и уважуха вам. Можно прочитать как сделать консольную напоминалку).
    Но можно не заниматься извращением, а сразу поставить специализированные "напоминалки". Вот несколько из них: LeaderTask, Simple Sticky Notes и т.п.
    Насчет работоспособности и функционала я не знаю что у них. Т.к. я не могу протестировать их.

     

    СМ.ТАКЖЕ

  16. Квантовый блог

    • 1
      запись
    • 9
      комментариев
    • 14091
      просмотр

    Последние записи

    Некоторые помнят мою передавалку чисел по рэдстоуну...
    Но чисел мало...
    Но теперь скорость настолько большая,что можно передавать даже файлы!
    Зато изменились протокол передачи и программа)
    Краткая характеристика:
    Поддержка юникода +
    Скорость передачи байта от 0,1 до 0,3 секунд


    Посмотреть код:

     

    Отправлялка:http://pastebin.com/Zdj8Gh5F
    Принималка:http://pastebin.com/bAACig6m

     

  17. Всем привет! Сегодня я расскажу Вам о том, как делать задания для игроков с помощью мода Custom Npcs. Для начала вам понадобится любой диалог ( подробнее тут

    ).

    1. Зайдём в меню диалога
    2. Создадим...."поддиалог"
    3. В поле "текст диалога" пишем что-то вроде "Принеси мне 1 блок обсидиана. я тебя за это щедро награжу, {player}!"
    4. Подключаем "поддиалог" так, чтобы у нас была возможность перейти на него
    5. Забываем на время про диалог


    Дальше делаем так Глобальные -> Задания -> Добавить -> лкм по созданному заданию -> кнопка Задания -> Добавить -> лкм по созданному окошку. Разберём его:

    • Название - просто название, чтобы не запутаться
    • Завершение текста - пишем туда что-то вроде "Ты сделал это! Держи награду!!!"
    • Текст квеста - описание задания
    • Награда - туда ставите то, что получит прошедший (можно оставить воздух)
    • Тип - есть задания на вещи, убийства, диалоги, локации и т.д. в зависимости от типа игрок надо с кем-то поговорить, что-нибудь принести и т.д.
    • Повторяется - да или нет - можно ли пройти задание несколько раз
    • Продвинутые - отношения с фракциями после прохождения, команда после прохождения, автоматически дать следующее задание


    Так разобрались! теперь пишем:

    1. "завершение диалога" - "Поздравляю! Сейчас достану награду."
    2. "текст квеста" - "Melancholy попросил принести блок обсидиана. Обещает вкусную плюху...."
    3. "награда" - кидаем 5 алмазов
    4. "тип" - разумеется на вещь", "редактировать" - положим блок обсидиана. "выдать предмет" - отберут ли у нас предмет. О том что такое "Урон" и "NBT" я рассказывать не буду
    5. Всё. Задание готово!


    Теперь надо это привязать. Выбираем "Задание" (диалог, про который мы на время забыли). нажимаем на "Выбрать квест". Выбираем наш квест.
    Наслаждаемся работой.
    PS
    Осталось рассказать о предметах и о малозначительных вкладках. Стоит ли продолжать?

  18. Krutoy
    Последняя запись

    Новости!

    • Теперь мой браузер будет называться "Арбузер", и будет выполнен в зеленоватых тонах.
    • Zer0Galaxy мне помогает, и уже набросал парсинг и поиск по самым простым селекторам в CSS. Думаю, ему для полной работы с CSS нужно будет написать еще разов в 6 больше кода.
    • Готовы первые наброски самого браузера без страниц. Закладки, навигация, строка пути.

    Кстати, вы можете посмотреть эмулятор экрана компьютера из OC, который можно открыть в браузере и даже посмотреть исходный код.

     

    9AtyDPm.png

     

    Ядро написано примерно на 15% пока что. В следующий раз буду отрисовывать элементы страниц, подгружая их стили.

  19. В прошлый раз мы научились подключаться к сети OpenNet, создавать простенький сайт и открывать его на локальном компьютере. Сегодня мы попытаемся получить доступ к сайту удаленно.

    Чтобы файл index стал доступен по сети, необходимо на сервере запустить специальную программу - WEB-сервер. В стандартный набор программ для работы с Сетью она не входит, но ее всегда можно скачать, выполнив команду:

    wget -f https://preview.c9.io/krutoy242/opennet/_source/WEB/WEBserver.lua webserv.lua

    Да, да, не удивляйтесь, именно wget, хоть в составе нашего компьютера и нет интернет-карты. После того, как мы подключились к Сети нам стали доступны все прелести интернет-карты даже при отсутствии оной, а всё благодаря крутому интернет-серверу, функционирующему в Сети. Pastebin, кстати, тоже работает.

    Загрузили webserver? Запускаем его. Мы должны увидеть вот такую картинку:

    blogentry-7-0-90118400-1435679055_thumb.png

    Запомним IP-адрес нашего сервера (выделено на картинке). Он понадобится в первое время для подключения к серверу. Теперь идем к другому компьютеру, подключенному к Сети, и проверяем наличие связи с сервером:

    ping c0b.9cf.a4f

    Если у Вас нет второго компьютера, Вы можете воспользоваться любым свободным в нашем датацентре.

    Связь есть? Запускаем браузер с указанием адреса нашего сервера.

    onBrowser c0b.9cf.a4f

    blogentry-7-0-91764200-1435679191_thumb.png

    Как видим, для открытия сайта по сети нет необходимости указывать не только папку /web, но и имя файла index. Дело в том, что папка /web считается корневой для нашего сайта. А если не указать имя файла, то по умолчанию сервер вернет файл index. Все остальные файлы придется указывать.

     

    Но я не хочу что бы к моему серверу обращались по ужасному IP-адресу. Хотелось бы имя покороче и лучше запоминаемое. Для этого нужно пройти регистрацию на DNS-сервере.

    Допустим, я хочу, что бы наш сервер назывался Zer0. Не слишком оригинально, но на первый раз пойдет. Имя это не должно содержать пробелы и конечно же должно быть уникальным, т.е. никто ранее не должен был зарегистрировать такое же имя. Так же не желательно, чтобы имя содержало точки и наклонные черты. В этом случае имя будет зарегистрировано, но в дальнейшем могут возникнуть проблемы с маршрутизацией.

    Как же происходит процедура регистрации? Можно, конечно, воспользоваться напрямую функциями DNS-сервера, описанными в теме http://computercraft.ru/topic/675-opennetoc-prodolzhenie/?do=findComment&comment=9097, но с некоторого момента я предпочитаю пользоваться утилитой setdns, которая входит в стандартный набор программ OpenNet.

    Эта утилита позволяет проверить не зарегистрировано ли еще DNS-имя, какие имена зарегистрированы на тот или иной IP, проводить собственно регистрацию или корректировать настройки уже зарегистрированного имени. В будущем планируется реализовать функцию удаления DNS-имени, но пока она не реализована.

    Первым делом убеждаемся, что выбранное имя еще не никем не занято (пункт 1).

    blogentry-7-0-81800300-1435679429_thumb.png

    Затем запускаем процедуру регистрации (пункт 3).

    При регистрации необходимо указать желаемое dns-имя, IP-адрес, с которым это имя будет ассоциировано, и пароль. Пароль понадобится, если мы заходим перерегистрировать имя на другой IP.

    blogentry-7-0-80094400-1435679450_thumb.png

    Если регистрация проводится с того компьютера, чей IP ассоциируется с dns-именем, на запрос IP можно ввести пустую строку.

    После регистрации выбираем пункт 0 для выхода из утилиты setdns.

    Теперь мы можем обращаться к серверу не по IP, а по удобному имени. Снова запустим webserver на нашем компьютере, а на соседнем

    onBrowser Zer0

    blogentry-7-0-28516400-1435679755_thumb.png

     

    В следующий раз я постараюсь рассказать как сделать наш сайт разноцветным и интерактивным.

    (продолжение следует)

  20. KelLiN' - блог

    • 1
      запись
    • 4
      комментария
    • 10656
      просмотров

    Последние записи

    KelLiN
    Последняя запись

    Ресурсов всегда не хватает. Что делать ?! Копать! Где копать ?! Вот сейчас то после прочтения данной записи мы и узнаем.

     

    Для добычи ресурсов будем использовать планшет. Сам по себе для добычи ресурсов он бесполезен, но если в него при сборке установить геосканер, то мы сможем узнавать где и сколько ресурсов находяться в породе, но с определенной в настройках сервера погрешносью. Как собирать планшет возможно расскажу потом.

     

    Использоваться будет команда сканера scan. Вот выдержка из вики:

    scan(x: number, y: number, [ignoreReplaceable: boolean]): table or nil, stringФункция сканирует "колонну" блоков в относительных координатах (x, y) и возвращает таблицу плотностей (с определенной погрешностью). В случае ошибки возвращает nil и ее текст.Координаты (0, 0) обозначают колонну блоков, в которой располагается сам сканер (32 блока вверх от него, и 32 блока вниз).

    От себя добавлю только то, что можно сканировать куб 64*64*64 , где центр куба - положение сканера. Положением сканера будет центр этого куба, тоесть 32 высота ( Также у нас на сервер насколько я понял куб будет 128*128 и высотой в 64 блока). На сканирование одного блока уходит 10 энергии. На один столб должно порядка 640.

     

    Для начала работы нам потребуется сам планшет со следующими минимальными компонентами:

    -видеокарта 1 уровня

    -монитор 1 уровня

    -клавиатура

    -геосканер

    -любые процессор, оперативная память, винчестер, bios и пр. .

     

    Для начала работы нам нужен планшет с записанной на диск программой. Я не использовал в планшете интернет-карту, а просто вставил текст программы нажатием средней кнопки мыши в открытый для редактирования файл. Ссылка на pastebin: http://pastebin.com/eJne1Dna . Код eJne1Dna

    c=        require("component")computer= require("computer")event=    require("event")os=       require("os")term =    require("term")gpu=c.gpus=c.geolyzerfunction intro()  print("Нажмите пробел для сканирования")  print("Нажмите q для выхода")  print("Нажмите с для очистки экрана")  print("Область сканирования 20 блоков на восток")endfunction scann()--сканирует область в 20 блоков от игрока в сторону севера.  local cx,cy=1,1  local onThatX=0;--количество ресурсов для данного столбца. Используется для отрисовки глубины копки для нового  local maxy=1;--положение курсора по окончании сканирования  for x=1,20 do    gpu.set(cx,cy,tostring(x));--текущий столбец    data=s.scan(x,0);-- х инкриментируется до 20, у=0 ширина сканирования 1.    local t=0;--"табулятор" для двухсимвольной глубины.    if x>9 then t=1 end    for d=1,32 do      if data[d]>2 then        -- в data записаны плотности блоков. >2 означает сообщать о блоках с плотностью более 2.        -- Весь диапазон от 0 до 99. 99 это вроде игрок. Все ресурсы примерно одинаковой плотности в районе 3.        computer.beep(2000,0.1)        if onThatX>0 then          cy=cy+1          if (32-d)>9 and t==0 then t=1 end;--смещаем курсор для печати на один столбец дальше из-за цифт больше 9.          if cy<15 then             gpu.set(cx,cy,tostring(math.floor(32-d)));--Печатает глубину на которую нужно копать вниз относительно начальной высоты игрока.          else            -- для 80*15 экрана. Для больших экранов можно изменить и убрать.            gpu.set(cx,15,tostring(math.floor(32-d)))          end        end        onThatX=onThatX+1       end    end    if t==1 then cx=cx+2 else cx=cx+1 end;t=0        if cy>maxy then maxy=cy end;--положение курсора при продолжениие печати о "нажмите enter для продолжения".    cy=1    onThatY=0;--обнуляем количество ресурсов для текущего столбца.  end  term.setCursor(1,maxy)  term.write("нажмите enter для продолжения")  io.read()  term.clear()  intro()endintro()while true do  _,_,key1,key2=event.pull("key_down")  if key2==57 then      term.clear();scann()  elseif key2==46 then  term.clear();intro()  elseif key2==16 then  term.clear();os.exit()  endend

    Для демонстрации работы я подготовил стэнд:

    index.php?app=core&module=attach&section=attach&attach_rel_module=post&attach_id=424

     

    С включенным планшетом я стал в позицию 1. Для работы программы нужно обязательно смотреть на восток. Это связанно с жестким закреплением направления сканирования в программе из-за избыточной сложности пользования gps (не сложности программирования, а малого gps.range() ). Скриншоты с начальным положением и направлением в начале работы для тех, кто путается со сторонами света или пока еще не проходил географию в школе:

    index.php?app=core&module=attach&section=attach&attach_rel_module=post&attach_id=425

     

    index.php?app=core&module=attach&section=attach&attach_rel_module=post&attach_id=427

     

    Далее запускаем программу и увидим небольшие инструкции для работы. Возможностей пока мало:

    -выход по нажатию на кнопку "q" .

    -сканирование по нажатию на кнопку пробела .

    -очистка экрана по нажатию на "c".

     

    Смело нажимаем пробел и программа начнёт сканировать породу под игроком на расстояние в 20 блоков на восток. В итоге у нас получиться примерно вот такая табличка, разобраться в которой я помогу на следующих скриншотах (По оси X удаление от игрока, по Y- глубина залегания добра).

    index.php?app=core&module=attach&section=attach&attach_rel_module=post&attach_id=426

     

    А вот и обьяснение как расшифровать эту табличку:

    index.php?app=core&module=attach&section=attach&attach_rel_module=post&attach_id=428

    Рассмотрим на примере золота. Нужно сделать 5 шагов вперед от начальной точки и прокопать на глубину минимум 4 блока. Если копать до 7го блока, то мы выкопаем все ресурсы .

     

    Как можно заметить, дерево, шерсть и губка имели плотность ниже 2х и не попали в табличку. Но попала лава и ядерный реактор.

     

    Обьяснение скудное, но заходите в игру и покажу.

    Программу можно модифицировать разными способами. Добавить сетку привязок высот, чтобы небыло мешанины цифр. Тогда можно будет определять лавовые озера по горизонтальному скоплению "ресурсов". С успеход дописывается работа с gps, тогда отпадает необходимость работы только в сторону востока. Но gps.range() у нас на сервере всего 64 блока, поэтому уйдя далее этого расстояния необходимо отрисовывать новую карту и вставлять её в gps приемник, что очень сильно напрягает. А так бы можно было сделать визуальное и звуковое оповещение над каким блоком копать вниз и на сколько. Визуально показывать на сколько копать вниз, звуком пищать на широте и долготе залегания ресурсов. При этом сам модуль gps ставить невнутри в робта, а в контейнер для улучшений,иначе нужно будет разбирать робота чтобы поменять карту.

     

    Еще опечалил тот факт, что роботам увеличили энергохранилище во много раз, а планшет судя по расходу энергии оставили на стандартном уровне. Поэтому планшет и без геосканера разряжается в теченииполучаса простого программирования в нём..Надеюсь поправят и запас энергии и дальность работы gps от одной карты . Собственно для привлечения внимания к этому факту и написана статья.

  21. 1Ridav' - блог

    • 1
      запись
    • 4
      комментария
    • 15442
      просмотра

    Последние записи

    1Ridav
    Последняя запись

    Пастбин для ОС компьютерной части на nKbGjVPw

     

    Разрабатываю удаленное управление компьютерами в игре через android/jar приложение.

    Ссылка на превью тему с андроид приложением: http://computercraft.ru/topic/347-android-opencomputers/

     

     

    API ОС части если и изменится, то крайне не существенно.

    Текущий API OC части:

     

    local br = require("bridge")

     

    br.init() - Создает соединение с мостом, позволяет использовать дальнейшие функции. Возвращает значение true/false через return. false вернется в случае неудачного соединения.

     

    br.auth("ключ в виде строки") - Производит авторизацию на мосту, позволяет найти соединение с партнером по ключу. Возращает значение true/false false вернется в случае неудачной попытки отправить ключ. Если мост найдет партнера с таким же ключем - мост пришлет сообщение CONNECTION WITH КЛЮЧ ESTABLISHED

     

    br.send("сообщение") - Посылает сообщение на мост, Если сообщение отослано нормально - вернется true через return функции. Если на мосту нет другого соединения с таким же ключом - мост пришлет сообщение I DO NOT HAVE A PAIR

     

    br.receive() - Блокирует процесс до тех пор, пока не придет сообщение от моста, возвращает два значения - true/false и message. true/false означает выполнилась ли функция нормально, message будет содержать сообщение от моста. Возможно значение nil, если соединение потеряно, даже, если первый аргумент будет true.

     

    br.finish() - Не имеет return значений, Закрывает соединение.

     

     

    Пример использования без параллельного запуска:

    local br = require("bridge")br.init() -- Соединяемся с мостомbr.auth("12345") -- Авторизируемся на мостуwhile true do   local status, message = br.receive() -- считываем ответ моста, ждем когда он найдет для нас партнера(НЕ ОБЯЗАТЕЛЬНО)   print(message)   br.send(io.read()) -- Пишем сообщение с клавиатуры и отправляем партнеру, если партнер не найден - мост об этом уведомит endbr.finish() -- ОБЯЗАТЕЛЬНО закрываем за собой соединение

×
×
  • Создать...