Jump to content

[Урок] loadfile / loadstring


Recommended Posts

Описание:

  • loadfile ( "путь_к_файлу" ) - это функция языка LUA, которая позволяет прочесть любой файл как LUA скрипт и загрузить его в текущий скрипт в виде функции.

    • Внимание: эта функция считается небезопасной с версии 1.1.
      Параметры:

      • "путь_к_файлу" - строка, содержащая путь к скрипту, начиная с папки, где находится EXEшник сервера, допустим, "mods/deathmatch/resources/имя_ресурса/имя_скрипта.lua", если скрипт лежит в папке ресурса, или "mods/deathmatch/имя_скрипта.lua", если скрипт лежит рядом с файлом настройки сервера.

    Возвращаемые значения:


    • функцию - если код файла был без ошибок
      nil и "строку с кодом ошибки" - если в коде файла найдены ошибки

[*]loadstring ( "строка_с_кодом" ) - это функция языка LUA, которая позволяет прочесть любую строку как LUA код и загрузить этот код в текущий скрипт в виде функции.


  • Параметры:

    • "строка_с_кодом" - любая строка, желательно с правильным LUA кодом

Возвращаемые значения:


  • функцию - если код в строке был без ошибок
    nil и "строку с кодом ошибки" - если найдены ошибки в коде строки

Области применения:

  • Динамическое подключение других скриптов в текущий скрипт без указания их в meta.xml
  • Выполнение кода, который хранится в виде строки, например, в базе данных, XML файле или другом месте
  • Изменение, проверка и запуск ваших скриптов прямо на сервере. Допустим, вы админ и хотите никуда не выходя с сервера изменить какой-то скрипт вашего ресурса: несколько махинаций с GUI и вы можете открывать/изменять/запускать нужные скрипты.
  • Хранение аккаунтов игроков, настроек, языковых файлов и прочего в виде LUA кода, например в виде таблиц
  • Проверка работоспособности кода перед его выполнением с возможностью узнать текст ошибки

Начнем с более простой loadstring:

-- в строке нет ошибок 
local sMyCode = 'local str = "qwerty" \n return str' 
local fGetQwerty = loadstring(sMyCode) 
outputChatBox( 'fGetQwerty(): "'..fGetQwerty()..'"' ) -- fGetQwerty(): "qwerty" 
  
-- код вверху аналогичен этому 
local function fGetQwerty() 
    local str = "qwerty" 
    return str 
end 
outputChatBox( 'fGetQwerty(): "'..fGetQwerty()..'"' ) -- fGetQwerty(): "qwerty" 
  
  
  
-- а теперь допустим в коде ошибку 
local sErrorText 
  
sMyCode = 'local str = "qwerty \n return str' -- не завершена строка 
fGetQwerty, sErrorText = loadstring(sMyCode) 
  
outputChatBox( tostring(fGetQwerty) ) -- nil 
outputChatBox(sErrorText) -- [string "local str = "qwerty ..."]:1: unfinished string near '"qwerty' 

Здесь мы взяли простую строку, которая может содержать правильный или неправильный LUA код. Загрузили эту строку как LUA код и поместили его в новую функцию, которую мы можем вызвать позднее, когда это нужно. Если код в строке был верный, то loadstring вернула нам переменную типа 'function' или проще говоря, новую функцию. Если код в строке был неверный, то первым значением loadstring вернула nil, а вторым - "строку с текстом ошибки".

Теперь loadfile:

-- допустим наш ресурс называется "myResource" 
-- в папке это ресурса есть два скрипта "main.lua" и "include.lua" 
-- в "meta.xml" мы указали только "main.lua" как серверный скрипт 
  
  
-- допустим, текст файла "include.lua" будет такой -------------- 
local t = {"qwe","rty","uio"} 
  
return t 
-------------------------- 
  
  
-- текст файла "main.lua" ------------------------ 
-- имя этого ресурса ("myResource") 
local sThisResourceName = getResourceName( getThisResource() ) 
-- путь к скрипту 
local sPathToFile = 'mods/deathmatch/resources/'..sThisResourceName..'/include.lua' 
  
-- загружаем файл и создаем из него функцию 
local fFunction, sErrorText = loadfile(sPathToFile) 
  
if not fFunction then -- если в коде файла есть ошибка или файл не существует 
    outputChatBox('Ошибка в коде файла "'..sPathToFile..'":') 
    outputChatBox('  '..sErrorText) 
else -- если всё пучком 
    local tTableFromFile = fFunction() -- выполним код файла 
    outputChatBox('  tTableFromFile[2] = "'..tTableFromFile[2]..'"') -- tTableFromFile[2] = "rty" 
end 
-------------------------- 

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

Хранение данных в виде LUA кода

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

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

Допустим файлы аккаунтов будут иметь следующий вид:

return { 
    armour = 33.4, 
    money = 423685, 
    password = "123456", 
    health = 74.6 
} 

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

-- общий список данных игроков 
local tPlayersData = {} 
  
-- пытаемся подключить файл аккаунта 
local fGetAccountData, sError = loadfile("путь/к/файлу/аккаунта") 
  
if fGetAccountData then -- если файл есть и загружен 
    -- player тут это какой-то игрок, допустим, который вошел на сервер 
    tPlayersData[player] = fGetAccountData() 
else 
    -- аккаунт не существует или ошибка в данных файла аккаунта 
    -- можно воспользоваться sError, чтобы определить причину 
    -- или вывести ее в лог 
end 
  
fGetAccountData = nil -- загруженный файл уже далее не нужен 
sError = nil -- текст ошибки тоже не нужен 

Прочесть файл аккаунта мы можем, а как же его изменить? Здесь как раз и нужна функция, которая из таблицы с данными аккаунта создаст правильный LUA код с определением этой таблицы. Далее мы к этому определению таблицы спереди добавим "return " и перезапишем весь файл этой строкой.

Начнем с функции конверта таблицы в строку правильного LUA кода:

-- заменяет все специальные символы в строке на лексемы 
local tEscapes = { 
    ["\a"] = '\\a', 
    ["\b"] = '\\b', 
    ["\f"] = '\\f', 
    ["\n"] = '\\n', 
    ["\r"] = '\\r', 
    ["\t"] = '\\t', 
    ["\\"] = '\\', 
    ["\""] = '\\"' 
} 
  
local function fSafeString ( s ) 
    for sSearch, sReplace in pairs(tEscapes) do 
        s = string.gsub( s, sSearch, sReplace ) 
    end 
  
    for n = 1, 31 do 
        s = string.gsub( s, string.char(n), '\\'..string.format('%03d',n) ) 
    end 
  
    return s 
end 
  
  
  
  
  
-- конверт таблицы в строку 
-- если bIndent = true, то сделает и корректные отступы с переносом на новую строку 
local sQuote = '"' 
local sIndent = "    " 
local sNewLine = "\r\n" 
local sLeftBracket, sRightBracket = '[', ']' 
local sLeftBrace, sRightBrace = '{', '}' 
local sEqual = '=' 
local sIndentedEqual = ' = ' 
local sSeparator = ',' 
  
local nRecurLevel = 0 
local nIndentLevel = 0 
local tRefs = {} 
  
local function fTableToString ( t, bIndent ) 
    if type(t) ~= 'table' then return tostring(t) end 
  
    nRecurLevel = nRecurLevel + 1 
    nIndentLevel = nIndentLevel + 1 
  
    local tResult, sType = {} 
  
  
  
    for k, v in pairs(t) do 
        sType = type(k) 
  
        if sType == 'string' then 
            k = fSafeString(k) 
            if not k:find('^%a%w*$') then 
                k = sLeftBracket .. sQuote .. k .. sQuote .. sRightBracket 
            end 
        elseif sType == 'number' then 
            k = sLeftBracket .. tostring(k) .. sRightBracket 
        else 
            k = sLeftBracket .. sQuote .. tostring(k) .. sQuote .. sRightBracket 
        end 
  
        sType = type(v) 
  
        if sType == 'string' then 
            v = sQuote .. fSafeString(v) .. sQuote 
        elseif sType == 'table' then 
            if tRefs[v] then 
                v = sQuote .. tostring(v) .. sQuote 
            else 
                v = fTableToString(v, bIndent) 
            end 
  
            tRefs[v] = true 
        elseif sType == 'number' then 
            v = tostring(v) 
        else 
            v = sQuote .. tostring(v) .. sQuote 
        end 
  
        if bIndent then 
            table.insert( tResult, string.rep(sIndent,nIndentLevel) .. k .. sIndentedEqual .. v ) 
        else 
            table.insert( tResult, k .. sEqual .. v ) 
        end 
    end 
  
  
  
    local sResult 
  
    if not bIndent then 
        sResult = sLeftBrace .. table.concat(tResult, sSeparator) .. sRightBrace 
    else 
        sResult = sLeftBrace .. sNewLine .. 
            table.concat(tResult, sSeparator..sNewLine) .. 
            sNewLine .. string.rep(sIndent,nIndentLevel-1) .. sRightBrace 
    end 
  
  
  
    if nRecurLevel <= 1 then tRefs = {} end 
  
    nRecurLevel = nRecurLevel - 1 
    nIndentLevel = nIndentLevel - 1 
  
    return sResult 
end 

Теперь мы можем изменить файл аккаунта:

local sAccountPath = "путь/к/файлу/аккаунта" 
  
-- удаляем файл аккаунта 
-- по желанию можно делать и бэкапы перед удалением 
fileDelete(sAccountPath) 
  
-- создаем новый файл аккаунта 
local uFile = fileCreate(sAccountPath) 
  
if not uFile then -- если не удалось создать 
    outputChatBox( 'Файла аккаунта не открывается для записи' ) 
    -- что-то еще 
else -- файл создался 
    -- записываем строку с определением таблицы в файл 
    if not fileWrite( uFile, 'return ' .. fTableToString(tPlayersData[player], true) ) then 
        outputChatBox( 'Строка данных аккаунта не записалась в файл' ) 
        -- что-то еще 
    end 
  
    fileClose(uFile) -- сохраним файл 
end 

Точно таким же образом мы можем хранить и любые другие данные - языковые файлы, настройки и прочее. t, bIndent

Edited by Guest
Link to comment
Loadfile will be disabled in a future release, it is not meant to be enabled at all.

You want say that loadfile will be disabled, but what this benefit???

loadfile can break outside mta folder, even though it only expects lua files and can't be too harmful, it's still suggested to disable it.

Link to comment
  • Other Languages Moderators
loadfile can break outside mta folder, even though it only expects lua files and can't be too harmful, it's still suggested to disable it.

Mean store by loadfile not secure?? What do you do? Store by SQLite nobody wants :D .

P.S Why we speak in English???

Link to comment

в 1,1 есть функции файлов в клиенте

ну если отключат обе, придется что-то вроде toJSON / fromJSON юзать + функции файлов для хранения файлов аккаунтов и прочих настроек. Создание/правку/удаление своих .DBшек sqlite еще никто не сделал

Link to comment
  • 4 weeks later...

Т.к. mtasa 1.1 уже ругается на использование loadfile, я написал небольшую функцию, которая использует loadstring и файловые функции.

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

function getScriptAsFunction ( sPath ) 
    if type(sPath) ~= 'string' then sPath = tostring(sPath) end 
  
  
    if not fileExists(sPath) then 
        return false, "can't find file '"..sPath.."'" 
    end 
  
  
    local uFile = fileOpen( sPath, true ) 
  
    if not uFile then 
        return false, "can't open file '"..sPath.."'" 
    end 
  
  
    local nFileSize = fileGetSize(uFile) 
  
    if not nFileSize then 
        fileClose(uFile) 
        return false, "can't get file '"..sPath.."' size" 
    end 
  
  
    local sFileContent = fileRead( uFile, nFileSize ) 
  
    if not sFileContent or #sFileContent <= 0 then 
        fileClose(uFile) 
        return false, "can't get file '"..sPath.."' text" 
    end 
  
    fileClose(uFile) 
  
  
    -- вырежем BOM, если он есть в начале файла 
    if string.byte( sFileContent, 1 ) == 0xEF then 
        sFileContent = string.sub( sFileContent, 4 ) 
    end 
  
  
    local f, sError = loadstring(sFileContent) 
  
    if type(f) ~= 'function' then 
        return false, sError 
    end 
  
  
    return f 
end 

Функция подходит для клиента и сервера версии 1.1, а также только для сервера версии 1.0.x.

Пример:

  • Допустим, в ресурсе "tools" лежит файл "tableTools.lua", который состоит из ваших дополнительных функций для работы с таблицами, которые собраны в одну таблицу (простейший класс). Вот пример этого класса:
    local tableTools = {} 
      
    function tableTools.findValueIndex(...) ... end 
    function tableTools.deleteValue(...) ... end 
    function tableTools.clear(...) ... end 
      
    return tableTools 
    


  • В meta.xml нашего конечного ресурса добавляем
    resource="tools" />


    чтобы ресурс, где лежит наш файл, запускался перед запуском конечного ресурса. Это важно, допустим, если вы будете подключать один клиентский скрипт из другого - он должен сначала скачаться.
    _

  • И далее мы должны получить эту таблицу функций в конечный ресурс, чтобы пользоваться этими функциями. Делаем так:
    local myTableTools 
    local myTableToolsFunction, errorText = getScriptAsFunction(":tools/tableTools.lua") 
      
    -- если файл успешно подключен как тело функции 
    if type(myTableToolsFunction) == "function" then 
        -- выполним ее и результатом должна быть таблица с нашими функциями 
        myTableTools = myTableToolsFunction() 
    else 
        outputDebugString( "Can't include file with my table tools! Reason: " .. errorText, 3 ) 
        return false -- выход со скрипта 
    end 
      
    -- если таблица не получена или в ней нет нужных функций 
    if type(myTableTools) ~= "table" or not myTableTools.findValueIndex then 
        outputDebugString( "Can't get table with my table tools!", 3 ) 
        return false -- выход со скрипта 
    end 
      
    -- здесь мы уже можем юзать нашу новую таблицу myTableTools с функциями внутри 
    -- ... 
    

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...