Неактуально Краш (Lua51: нарушение прав доступа)

NyashMyash99

Известный
Автор темы
36
49
Версия MoonLoader
.026-beta
РЕШЕНИЕ: Избавился от потоков по совету wojciech? и ошибка перестала появляться.

Сделал скрипт, улучшающий интерфейс на Arizona RP, и теперь стабильно ловлю краши, помогите, люди добрые :')

Как я предполагаю, это случается из-за отмены рендера диалога через sampev.onShowDialog, когда возвращаешь false в качестве результата, причём без какой-то закономерности, но глубже докопаться не могу, ибо до этого ничего под SAMP не писал ( к слову, буду рад конструктивной критике от олдов скриптописания ).

Скрипт:
Lua:
-- Imports
require 'lib.moonloader'
local sampev = require 'lib.samp.events'
local gui = require 'mimgui'
local icons = require 'lib.fAwesome6'
local encoding = require 'encoding'

encoding.default = 'CP1251'
local u8 = encoding.UTF8

-- Script directives
script_name('Player Information Collector')
script_description('Display useful information about account statistics in interface')
script_version('1.0.0b')
script_author('NyashMyash99')

-- Variables
local updatePeriod = 60 * 1000
local isUpdateEnabled = false

local quests = {}
local questsKeys = {}

local selectedQuest = nil
local selectedQuestIndex = -1

local playerData = {
    level = 0,
    exp = 0,
    nextExp = 0,
    bankMoney = 0,
    depositeMoney = 0
}

local font = {}
local targetDialog = -1
local isLoaded = false
local debug = true

function main()
    sendDebug('[main] Waiting for game client to be ready.')
    while not isSampAvailable() do wait(500) end
    sendDebug('[main] Game client is ready, start initializing script.')

    sendMessage('')
    sendMessage(script.this.name .. ' - ' .. table.concat(script.this.authors, ', ') .. ' (' .. script.this.version .. ')')
    sendMessage(script.this.description)
    sendMessage('')

    while true do
        -- switch active quest
        if isKeyJustPressed(VK_NUMPAD4) then
            sendDebug('[NUMPAD 4] Key is pressed.')

            sendDebug('[NUMPAD 4] Past index of active quest: ' .. tostring(selectedQuestIndex) .. '.')
            selectedQuestIndex = selectedQuestIndex - 1
            sendDebug('[NUMPAD 4] Expected index of active quest: ' .. tostring(selectedQuestIndex) .. '.')

            -- go to opposite end of list
            if selectedQuestIndex < 1 then
                sendDebug('[NUMPAD 4] Expected active quest index is less than one, change to "' .. tostring(#questsKeys) .. '".')
                selectedQuestIndex = #questsKeys
            end

            sendDebug('[NUMPAD 4] New index of active quest: ' .. tostring(selectedQuestIndex) .. '/' .. tostring(#questsKeys) .. '.')
            sendDebug('[NUMPAD 4] New active quest: ' .. tostring(questsKeys[selectedQuestIndex]) .. '.')
            selectedQuest = questsKeys[selectedQuestIndex]
        end

        -- switch active quest
        if isKeyJustPressed(VK_NUMPAD6) then
            sendDebug('[NUMPAD 6] Key is pressed.')

            sendDebug('[NUMPAD 6] Past index of active quest: ' .. tostring(selectedQuestIndex) .. '.')
            selectedQuestIndex = selectedQuestIndex + 1
            sendDebug('[NUMPAD 6] Expected index of active quest: ' .. tostring(selectedQuestIndex) .. '.')

            -- go to opposite end of list
            if selectedQuestIndex > #questsKeys then
                sendDebug('[NUMPAD 6] Expected active quest index is greater than number of quests, change to "1".')
                selectedQuestIndex = 1
            end

            sendDebug('[NUMPAD 6] New index of active quest: ' .. tostring(selectedQuestIndex) .. '/' .. tostring(#questsKeys) .. '.')
            sendDebug('[NUMPAD 6] New active quest: ' .. tostring(questsKeys[selectedQuestIndex]) .. '.')
            selectedQuest = questsKeys[selectedQuestIndex]
        end

        -- toggle auto-update function
        if isKeyJustPressed(VK_NUMPAD5) then
            sendDebug('[NUMPAD 5] Key is pressed.')

            sendDebug('[NUMPAD 5] Past "isUpdateEnabled" value: ' .. tostring(isUpdateEnabled) .. '.')
            isUpdateEnabled = not isUpdateEnabled
            sendDebug('[NUMPAD 5] New "isUpdateEnabled" value: ' .. tostring(isUpdateEnabled) .. '.')

            local statusMessage = isUpdateEnabled and '{00FF00}enabled' or '{FF0000}disabled'
            sendMessage('Automatic data update ' .. statusMessage)

            -- if task has been disabled - start it
            if isUpdateEnabled then
                sendDebug('[NUMPAD 5] Update task was previously disabled - enable it.')
                updatePlayerData()
            end
        end

        -- early data update
        if isKeyJustPressed(VK_NUMPAD0) then
            sendDebug('[NUMPAD 0] Key is pressed.')

            sendDebug('[NUMPAD 0] Force updating player data.')
            updatePlayerData(true)

            sendMessage("Updating player data..")
        end

        -- switch interface visibility (mostly needed for script hot rebooting)
        if isKeyJustPressed(VK_SUBTRACT) then
            sendDebug('[NUMPAD -] Key is pressed.')

            sendDebug('[NUMPAD -] Past "isLoaded" value: ' .. tostring(isLoaded) .. '.')
            isLoaded = not isLoaded
            sendDebug('[NUMPAD -] New "isLoaded" value: ' .. tostring(isLoaded) .. '.')
        end

        wait(0)
    end
end

-- GUI
gui.OnInitialize(function()
    -- disable ini config. By default it is saved to moonloader/config/mimgui/%scriptfilename%.ini
    gui.GetIO().IniFilename = nil

    -- clear loaded fonts to make ours main font
    sendDebug('[OnInitialize] Cleaning up old fonts.')
    gui.GetIO().Fonts:Clear()
    sendDebug('[OnInitialize] Old fonts cleaned.')

    -- pulling font from arizona launcher
    local fontPath = getWorkingDirectory() .. '\\..\\frontend\\vue_js\\fonts\\'
    local fontFile = 'SFProDisplay-Semibold.0538ddc9.ttf'

    sendDebug('[OnInitialize] Installing "' .. tostring(fontPath) .. tostring(fontFile) .. '" font.')
    font[18] = gui.GetIO().Fonts:AddFontFromFileTTF(
        fontPath .. fontFile,
        18,
        _, gui.GetIO().Fonts:GetGlyphRangesCyrillic()
    )
    sendDebug('[OnInitialize] Font "' .. tostring(fontPath) .. tostring(fontFile) .. '" installed.')

    fontFile = 'SFProDisplay-Regular.6987bcc4.ttf'

    sendDebug('[OnInitialize] Installing "' .. tostring(fontPath) .. tostring(fontFile) .. '" font.')
    font[30] = gui.GetIO().Fonts:AddFontFromFileTTF(
        fontPath .. fontFile,
        30,
        _, gui.GetIO().Fonts:GetGlyphRangesCyrillic()
    )
    sendDebug('[OnInitialize] Font "' .. tostring(fontPath) .. tostring(fontFile) .. '" installed.')

    loadIconicFont()

    -- font texture invalidation forces the font texture to rebuild. It is necessary after font modifications
    sendDebug('[OnInitialize] Invalidating fonts texture.')
    gui.InvalidateFontsTexture()
    sendDebug('[OnInitialize] Fonts texture invalidated.')

    -- run interface update
    sendDebug('[OnInitialize] Updating player data.')
    updatePlayerData()
end)

--- render interface if player has already loaded and isn't in game menu
gui.OnFrame(
    function() return not isPauseMenuActive() and isLoaded end,
    function() renderInterface() end
).HideCursor = true

function loadIconicFont()
    local config = gui.ImFontConfig()
    config.MergeMode = true
    config.PixelSnapH = true

    sendDebug('[loadIconicFont] Installing "fa-icons" font.')
    font['icon'] = gui.GetIO().Fonts:AddFontFromMemoryCompressedBase85TTF(
        icons.get_font_data_base85('bold'),
        24,
        config,
        gui.new.ImWchar[3](icons.min_range, icons.max_range, 0)
    )
    sendDebug('[loadIconicFont] "fa-icons" font installed.')
end

-- Event handlers
--- waits for a special message to activate interface display
function sampev.onServerMessage(_, text)
    if stripColors(text) == '[Подсказка] На сервере есть инвентарь, используйте клавишу Y для работы с ним.' then
        sendDebug('[onServerMessage] Successful load message received.')
        isLoaded = true
    end
end

--- handles dialogs without showing them to player if they were called by a script
function sampev.onShowDialog(id, _, _, _, _, text)
    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Dialog received.')

    if id ~= targetDialog then
        sendDebug('[onShowDialog] (' .. tostring(id) .. ') Showing dialog.')
        return true
    end

    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Proccessing dialog.')
    proccessTargetDialog(id, text)

    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Canceling dialog.')
    return false
end

-- Functions
--- starts task of updating data once in updatePeriod
function updatePlayerData(force)
    lua_thread.create(
        function()
            sendDebug('[updatePlayerData] Update request received.')

            sendDebug('[updatePlayerData] Starting collecting statistics.')
            openStatisticDialog()

            -- waiting for dialog to be processed
            while targetDialog ~= -1 do
                sendDebug('[updatePlayerData] Statistics collection isn\'t yet complete, pending.')
                wait(300)
            end

            sendDebug('[updatePlayerData] Starting collecting quests.')
            openActiveQuestsDialog()

            -- waiting for dialog to be processed
            while targetDialog ~= -1 do
                sendDebug('[updatePlayerData] Quests collection isn\'t yet complete, pending.')
                wait(300)
            end

            -- waiting for next renewal period
            sendDebug('[updatePlayerData] Waiting for next update task.')
            wait(updatePeriod)
            sendDebug('[updatePlayerData] Waited for next update task.')

            -- if auto-update is enabled - start task again
            if not force and isUpdateEnabled then
                sendDebug('[updatePlayerData] Starting new update task (force = ' .. tostring(force) .. ', isUpdateEnabled = ' .. tostring(isUpdateEnabled) .. ').')
                updatePlayerData()
            end
        end
    )
end

function openActiveQuestsDialog()
    lua_thread.create(
        function()
            sendDebug('[openActiveQuestsDialog] Discovery request received.')

            -- preventing dialog race
            while targetDialog ~= -1 do
                sendDebug('[openActiveQuestsDialog] Waiting for queue to be released.')
                wait(50)
            end

            -- target 'Quests' dialog and enter command
            targetDialog = 25862

            sendDebug('[openActiveQuestsDialog] Dispatching command.')
            sampSendChat('/quest')
            sendDebug('[openActiveQuestsDialog] Command dispatched.')
        end
    )
end

function openStatisticDialog()
    lua_thread.create(
        function()
            sendDebug('[openStatisticDialog] Discovery request received.')

            -- preventing dialog race
            while targetDialog ~= -1 do
                sendDebug('[openStatisticDialog] Waiting for queue to be released.')
                wait(50)
            end

            -- target 'My Statistics' dialog and enter command
            targetDialog = 235

            sendDebug('[openStatisticDialog] Dispatching command.')
            sampSendChat('/stats')
            sendDebug('[openStatisticDialog] Command dispatched.')
        end
    )
end

function proccessTargetDialog(id, content)
    sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Processing request received.')

    -- if current dialog is 'Quests'..
    if id == 25862 then
        -- target 'Active quests' dialog and select corresponding menu item
        targetDialog = 7650

        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Sending menu click.')
        sampSendDialogResponse(25862, 1, 0, nil)
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Menu click sent.')
        return
    end

    -- if current dialog is 'Active quests'..
    if id == 7650 then
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Prepating dialog content.')
        local preparedContent = split(content, '\n')
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialog content prepared.')

        -- clearing old data
        quests = {}
        questsKeys = {}

        -- run through all lines of dialog
        for _, value in ipairs(preparedContent) do
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing value: ' .. tostring(value) .. '.')

            -- remove colors from text
            value = stripColors(value)
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Stripped value: ' .. tostring(value) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching questTitle.')
            local questTitle = value:match('(.+) |')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') questTitle: ' .. tostring(questTitle) .. '.')

            -- if we find title of quest - delete category from there
            if questTitle then
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing title: ' .. tostring(questTitle) .. '.')
                questTitle = questTitle:gsub('%[.+%]', '')
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessed title: ' .. tostring(questTitle) .. '.')
            end

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching questMatch.')
            local questMatch = value:match('Прогресс: (.+)')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') questMatch: ' .. tostring(questMatch) .. '.')

            -- if we find progress - save quest
            if questMatch then
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing progress: ' .. tostring(questMatch) .. '.')
                questMatch = questMatch:gsub('[%[%]]', '')
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessed progress: ' .. tostring(questMatch) .. '.')

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found "' .. tostring(questTitle) .. '" quest with progress: ' .. tostring(questMatch) .. '.')

                quests[questTitle] = questMatch
                -- save key, which will be useful for switching active quest
                table.insert(questsKeys, questTitle)

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Saved "' .. tostring(questTitle) .. '" quest with progress: ' .. tostring(questMatch) .. '.')
            end
        end

        -- if active quest isn't selected or deleted - select first found one
        if not quests[selectedQuest] then
            sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Updating active quest.')

            selectedQuest = questsKeys[1]
            selectedQuestIndex = 1

            sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') New active quest selected: ' .. tostring(selectedQuest) .. '.')
        end

        -- release pending dialog
        targetDialog = -1
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialogue capture released.')
        return
    end

    -- if current dialog is 'My Statistics'..
    if id == 235 then
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Prepating dialog content.')
        local preparedContent = split(content, '\n')
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialog content prepared.')

        -- run through all lines of dialog
        for _, value in ipairs(preparedContent) do
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing value: ' .. tostring(value) .. '.')

            -- remove colors from text
            value = stripColors(value)
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Stripped value: ' .. tostring(value) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching levelMatch.')
            local levelMatch = value:match('Уровень: %[(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') levelMatch: ' .. tostring(levelMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching respectMatch.')
            local respectMatch = value:match('Уважение: %[(%d+/%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') respectMatch: ' .. tostring(respectMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching bankMoneyMatch.')
            local bankMoneyMatch = value:match('Деньги в банке: %[%$(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') bankMoneyMatch: ' .. tostring(bankMoneyMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching depositeMoneyMatch.')
            local depositeMoneyMatch = value:match('Деньги на депозите: %[%$(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') depositeMoneyMatch: ' .. tostring(depositeMoneyMatch) .. '.')

            -- if we find a level - save it
            if levelMatch then
                local level = tonumber(levelMatch)
                playerData.level = level

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found level: ' .. tostring(level) .. '.')
            end

            -- if we find a respect - save it
            if respectMatch then
                local exp = tonumber(respectMatch:match('%d+'))
                local nextExp = tonumber(respectMatch:match('/(%d+)'))

                playerData.exp = exp
                playerData.nextExp = nextExp

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found respect: ' .. tostring(exp) .. '/' .. tostring(nextExp) .. '.')
            end

            -- if we find a bank money - save it
            if bankMoneyMatch then
                local money = tonumber(bankMoneyMatch)
                playerData.bankMoney = money

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found bank money: ' .. tostring(money) .. '.')
            end

            -- if we find a deposite money - save it
            if depositeMoneyMatch then
                local money = tonumber(depositeMoneyMatch)
                playerData.depositeMoney = money

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found deposite money: ' .. tostring(money) .. '.')
            end
        end

        -- release pending dialog
        targetDialog = -1
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialogue capture released.')
        return
    end
end


--- render interface with player data
function renderInterface()
    local windowWidth = getScreenResolution()
    local overlay = gui.GetBackgroundDrawList()

    -- quests background
    overlay:AddRectFilled(
        gui.ImVec2(windowWidth - 600, 97),
        gui.ImVec2(windowWidth - 395, 167),
        0xE6090909,
        16, 1 + 4
    )

    -- level
    overlay:AddText(
       gui.ImVec2(windowWidth - 600, 21.5),
       0xFFA5A6A5,
       'LVL:'
    )
    overlay:AddText(
       gui.ImVec2(windowWidth - 566, 21.5),
       0xFFFFFFFF,
       tostring(playerData.level)
    )

    -- exp
    overlay:AddText(
       gui.ImVec2(windowWidth - 600, 49.5),
       0xFFA5A6A5,
       'EXP:'
    )
    overlay:AddText(
       gui.ImVec2(windowWidth - 566, 49.5),
       0xFFFFFFFF,
       tostring(playerData.exp) .. '/' .. tostring(playerData.nextExp)
    )

    -- quests
    if selectedQuest then
        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 112),
            0xFFFFFFFF,
            u8(overlap(tostring(selectedQuest)))
        )

        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 132),
            0xFFFFFFFF,
            u8(overlap(tostring(quests[selectedQuest])))
        )
    else
        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 122),
            0xFFFFFFFF,
            u8'Загрузка квестов..'
        )
    end


    gui.PushFont(font['icon'])

    -- bank money
    overlay:AddText(
       gui.ImVec2(windowWidth - 240, 225),
       0xFF68FF65,
       icons['BUILDING_COLUMNS']
    )

    -- deposite money
    overlay:AddText(
       gui.ImVec2(windowWidth - 242, 265),
       0xFF68FF65,
       icons['PIGGY_BANK']
    )

    gui.PopFont()


    gui.PushFont(font[30])

    -- bank money
    overlay:AddText(
       gui.ImVec2(windowWidth - 210, 225),
       0xFFFFFFFF,
       tostring(formatMoney(tostring(playerData.bankMoney)))
    )

    -- deposite money
    overlay:AddText(
       gui.ImVec2(windowWidth - 210, 265),
       0xFFFFFFFF,
       tostring(formatMoney(tostring(playerData.depositeMoney)))
    )

    gui.PopFont()
end


--- sends script-style message to player chat
function sendMessage(message)
    sampAddChatMessage('{FFA500}' .. tostring(message), 0xFFA500)
end

--- sends debug message to console if debug is enabled
function sendDebug(message)
    if not debug then return end
    print(tostring(message))
end


--- separates string by delimiter
function split(str, delimiter)
    local result = {}

    for match in (str .. delimiter):gmatch('(.-)' .. delimiter) do
        table.insert(result, match)
    end

    return result
end

--- shortens string if it is longer than 20 characters
function overlap(text)
    if #text <= 20 then
        return text
    end

    -- get first 20 characters and add ..
    return text:sub(1, 20) .. '..'
end

--- сonverts number into readable monetary format
function formatMoney(amount)
    return tostring(amount)
        :reverse()
        :gsub("%d%d%d", "%1 ")
        :reverse()
        :gsub("^%s+", "")
end

--- removes SAMP colors from text
function stripColors(text)
    return text:gsub('{%x+}', '')
end

Логи moonloader с дебагом ( нас интересует в основном самое начало и конец ): https://pastebin.com/Cu8X9LKX

Отчёт краша: https://crash.sr.team/?uid=52e1786e-46482b32-56e669f9-79dcefad-dae3f8a8-7f24385d-ef42c033-999e6ed5

Скриншот краша:
K7U7OO1dWPY.jpg
 
Последнее редактирование:

SeregaIvanovis

Активный
118
33
Сделал скрипт, улучшающий интерфейс на Arizona RP, и теперь стабильно ловлю краши, помогите, люди добрые :')

Как я предполагаю, это случается из-за отмены рендера диалога через sampev.onShowDialog, когда возвращаешь false в качестве результата, причём без какой-то закономерности, но глубже докопаться не могу, ибо до этого ничего под SAMP не писал ( к слову, буду рад конструктивной критике от олдов скриптописания ).

Скрипт:
Lua:
-- Imports
require 'lib.moonloader'
local sampev = require 'lib.samp.events'
local gui = require 'mimgui'
local icons = require 'lib.fAwesome6'
local encoding = require 'encoding'

encoding.default = 'CP1251'
local u8 = encoding.UTF8

-- Script directives
script_name('Player Information Collector')
script_description('Display useful information about account statistics in interface')
script_version('1.0.0b')
script_author('NyashMyash99')

-- Variables
local updatePeriod = 60 * 1000
local isUpdateEnabled = false

local quests = {}
local questsKeys = {}

local selectedQuest = nil
local selectedQuestIndex = -1

local playerData = {
    level = 0,
    exp = 0,
    nextExp = 0,
    bankMoney = 0,
    depositeMoney = 0
}

local font = {}
local targetDialog = -1
local isLoaded = false
local debug = true

function main()
    sendDebug('[main] Waiting for game client to be ready.')
    while not isSampAvailable() do wait(500) end
    sendDebug('[main] Game client is ready, start initializing script.')

    sendMessage('')
    sendMessage(script.this.name .. ' - ' .. table.concat(script.this.authors, ', ') .. ' (' .. script.this.version .. ')')
    sendMessage(script.this.description)
    sendMessage('')

    while true do
        -- switch active quest
        if isKeyJustPressed(VK_NUMPAD4) then
            sendDebug('[NUMPAD 4] Key is pressed.')

            sendDebug('[NUMPAD 4] Past index of active quest: ' .. tostring(selectedQuestIndex) .. '.')
            selectedQuestIndex = selectedQuestIndex - 1
            sendDebug('[NUMPAD 4] Expected index of active quest: ' .. tostring(selectedQuestIndex) .. '.')

            -- go to opposite end of list
            if selectedQuestIndex < 1 then
                sendDebug('[NUMPAD 4] Expected active quest index is less than one, change to "' .. tostring(#questsKeys) .. '".')
                selectedQuestIndex = #questsKeys
            end

            sendDebug('[NUMPAD 4] New index of active quest: ' .. tostring(selectedQuestIndex) .. '/' .. tostring(#questsKeys) .. '.')
            sendDebug('[NUMPAD 4] New active quest: ' .. tostring(questsKeys[selectedQuestIndex]) .. '.')
            selectedQuest = questsKeys[selectedQuestIndex]
        end

        -- switch active quest
        if isKeyJustPressed(VK_NUMPAD6) then
            sendDebug('[NUMPAD 6] Key is pressed.')

            sendDebug('[NUMPAD 6] Past index of active quest: ' .. tostring(selectedQuestIndex) .. '.')
            selectedQuestIndex = selectedQuestIndex + 1
            sendDebug('[NUMPAD 6] Expected index of active quest: ' .. tostring(selectedQuestIndex) .. '.')

            -- go to opposite end of list
            if selectedQuestIndex > #questsKeys then
                sendDebug('[NUMPAD 6] Expected active quest index is greater than number of quests, change to "1".')
                selectedQuestIndex = 1
            end

            sendDebug('[NUMPAD 6] New index of active quest: ' .. tostring(selectedQuestIndex) .. '/' .. tostring(#questsKeys) .. '.')
            sendDebug('[NUMPAD 6] New active quest: ' .. tostring(questsKeys[selectedQuestIndex]) .. '.')
            selectedQuest = questsKeys[selectedQuestIndex]
        end

        -- toggle auto-update function
        if isKeyJustPressed(VK_NUMPAD5) then
            sendDebug('[NUMPAD 5] Key is pressed.')

            sendDebug('[NUMPAD 5] Past "isUpdateEnabled" value: ' .. tostring(isUpdateEnabled) .. '.')
            isUpdateEnabled = not isUpdateEnabled
            sendDebug('[NUMPAD 5] New "isUpdateEnabled" value: ' .. tostring(isUpdateEnabled) .. '.')

            local statusMessage = isUpdateEnabled and '{00FF00}enabled' or '{FF0000}disabled'
            sendMessage('Automatic data update ' .. statusMessage)

            -- if task has been disabled - start it
            if isUpdateEnabled then
                sendDebug('[NUMPAD 5] Update task was previously disabled - enable it.')
                updatePlayerData()
            end
        end

        -- early data update
        if isKeyJustPressed(VK_NUMPAD0) then
            sendDebug('[NUMPAD 0] Key is pressed.')

            sendDebug('[NUMPAD 0] Force updating player data.')
            updatePlayerData(true)

            sendMessage("Updating player data..")
        end

        -- switch interface visibility (mostly needed for script hot rebooting)
        if isKeyJustPressed(VK_SUBTRACT) then
            sendDebug('[NUMPAD -] Key is pressed.')

            sendDebug('[NUMPAD -] Past "isLoaded" value: ' .. tostring(isLoaded) .. '.')
            isLoaded = not isLoaded
            sendDebug('[NUMPAD -] New "isLoaded" value: ' .. tostring(isLoaded) .. '.')
        end

        wait(0)
    end
end

-- GUI
gui.OnInitialize(function()
    -- disable ini config. By default it is saved to moonloader/config/mimgui/%scriptfilename%.ini
    gui.GetIO().IniFilename = nil

    -- clear loaded fonts to make ours main font
    sendDebug('[OnInitialize] Cleaning up old fonts.')
    gui.GetIO().Fonts:Clear()
    sendDebug('[OnInitialize] Old fonts cleaned.')

    -- pulling font from arizona launcher
    local fontPath = getWorkingDirectory() .. '\\..\\frontend\\vue_js\\fonts\\'
    local fontFile = 'SFProDisplay-Semibold.0538ddc9.ttf'

    sendDebug('[OnInitialize] Installing "' .. tostring(fontPath) .. tostring(fontFile) .. '" font.')
    font[18] = gui.GetIO().Fonts:AddFontFromFileTTF(
        fontPath .. fontFile,
        18,
        _, gui.GetIO().Fonts:GetGlyphRangesCyrillic()
    )
    sendDebug('[OnInitialize] Font "' .. tostring(fontPath) .. tostring(fontFile) .. '" installed.')

    fontFile = 'SFProDisplay-Regular.6987bcc4.ttf'

    sendDebug('[OnInitialize] Installing "' .. tostring(fontPath) .. tostring(fontFile) .. '" font.')
    font[30] = gui.GetIO().Fonts:AddFontFromFileTTF(
        fontPath .. fontFile,
        30,
        _, gui.GetIO().Fonts:GetGlyphRangesCyrillic()
    )
    sendDebug('[OnInitialize] Font "' .. tostring(fontPath) .. tostring(fontFile) .. '" installed.')

    loadIconicFont()

    -- font texture invalidation forces the font texture to rebuild. It is necessary after font modifications
    sendDebug('[OnInitialize] Invalidating fonts texture.')
    gui.InvalidateFontsTexture()
    sendDebug('[OnInitialize] Fonts texture invalidated.')

    -- run interface update
    sendDebug('[OnInitialize] Updating player data.')
    updatePlayerData()
end)

--- render interface if player has already loaded and isn't in game menu
gui.OnFrame(
    function() return not isPauseMenuActive() and isLoaded end,
    function() renderInterface() end
).HideCursor = true

function loadIconicFont()
    local config = gui.ImFontConfig()
    config.MergeMode = true
    config.PixelSnapH = true

    sendDebug('[loadIconicFont] Installing "fa-icons" font.')
    font['icon'] = gui.GetIO().Fonts:AddFontFromMemoryCompressedBase85TTF(
        icons.get_font_data_base85('bold'),
        24,
        config,
        gui.new.ImWchar[3](icons.min_range, icons.max_range, 0)
    )
    sendDebug('[loadIconicFont] "fa-icons" font installed.')
end

-- Event handlers
--- waits for a special message to activate interface display
function sampev.onServerMessage(_, text)
    if stripColors(text) == '[Подсказка] На сервере есть инвентарь, используйте клавишу Y для работы с ним.' then
        sendDebug('[onServerMessage] Successful load message received.')
        isLoaded = true
    end
end

--- handles dialogs without showing them to player if they were called by a script
function sampev.onShowDialog(id, _, _, _, _, text)
    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Dialog received.')

    if id ~= targetDialog then
        sendDebug('[onShowDialog] (' .. tostring(id) .. ') Showing dialog.')
        return true
    end

    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Proccessing dialog.')
    proccessTargetDialog(id, text)

    sendDebug('[onShowDialog] (' .. tostring(id) .. ') Canceling dialog.')
    return false
end

-- Functions
--- starts task of updating data once in updatePeriod
function updatePlayerData(force)
    lua_thread.create(
        function()
            sendDebug('[updatePlayerData] Update request received.')

            sendDebug('[updatePlayerData] Starting collecting statistics.')
            openStatisticDialog()

            -- waiting for dialog to be processed
            while targetDialog ~= -1 do
                sendDebug('[updatePlayerData] Statistics collection isn\'t yet complete, pending.')
                wait(300)
            end

            sendDebug('[updatePlayerData] Starting collecting quests.')
            openActiveQuestsDialog()

            -- waiting for dialog to be processed
            while targetDialog ~= -1 do
                sendDebug('[updatePlayerData] Quests collection isn\'t yet complete, pending.')
                wait(300)
            end

            -- waiting for next renewal period
            sendDebug('[updatePlayerData] Waiting for next update task.')
            wait(updatePeriod)
            sendDebug('[updatePlayerData] Waited for next update task.')

            -- if auto-update is enabled - start task again
            if not force and isUpdateEnabled then
                sendDebug('[updatePlayerData] Starting new update task (force = ' .. tostring(force) .. ', isUpdateEnabled = ' .. tostring(isUpdateEnabled) .. ').')
                updatePlayerData()
            end
        end
    )
end

function openActiveQuestsDialog()
    lua_thread.create(
        function()
            sendDebug('[openActiveQuestsDialog] Discovery request received.')

            -- preventing dialog race
            while targetDialog ~= -1 do
                sendDebug('[openActiveQuestsDialog] Waiting for queue to be released.')
                wait(50)
            end

            -- target 'Quests' dialog and enter command
            targetDialog = 25862

            sendDebug('[openActiveQuestsDialog] Dispatching command.')
            sampSendChat('/quest')
            sendDebug('[openActiveQuestsDialog] Command dispatched.')
        end
    )
end

function openStatisticDialog()
    lua_thread.create(
        function()
            sendDebug('[openStatisticDialog] Discovery request received.')

            -- preventing dialog race
            while targetDialog ~= -1 do
                sendDebug('[openStatisticDialog] Waiting for queue to be released.')
                wait(50)
            end

            -- target 'My Statistics' dialog and enter command
            targetDialog = 235

            sendDebug('[openStatisticDialog] Dispatching command.')
            sampSendChat('/stats')
            sendDebug('[openStatisticDialog] Command dispatched.')
        end
    )
end

function proccessTargetDialog(id, content)
    sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Processing request received.')

    -- if current dialog is 'Quests'..
    if id == 25862 then
        -- target 'Active quests' dialog and select corresponding menu item
        targetDialog = 7650

        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Sending menu click.')
        sampSendDialogResponse(25862, 1, 0, nil)
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Menu click sent.')
        return
    end

    -- if current dialog is 'Active quests'..
    if id == 7650 then
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Prepating dialog content.')
        local preparedContent = split(content, '\n')
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialog content prepared.')

        -- clearing old data
        quests = {}
        questsKeys = {}

        -- run through all lines of dialog
        for _, value in ipairs(preparedContent) do
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing value: ' .. tostring(value) .. '.')

            -- remove colors from text
            value = stripColors(value)
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Stripped value: ' .. tostring(value) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching questTitle.')
            local questTitle = value:match('(.+) |')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') questTitle: ' .. tostring(questTitle) .. '.')

            -- if we find title of quest - delete category from there
            if questTitle then
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing title: ' .. tostring(questTitle) .. '.')
                questTitle = questTitle:gsub('%[.+%]', '')
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessed title: ' .. tostring(questTitle) .. '.')
            end

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching questMatch.')
            local questMatch = value:match('Прогресс: (.+)')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') questMatch: ' .. tostring(questMatch) .. '.')

            -- if we find progress - save quest
            if questMatch then
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing progress: ' .. tostring(questMatch) .. '.')
                questMatch = questMatch:gsub('[%[%]]', '')
                -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessed progress: ' .. tostring(questMatch) .. '.')

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found "' .. tostring(questTitle) .. '" quest with progress: ' .. tostring(questMatch) .. '.')

                quests[questTitle] = questMatch
                -- save key, which will be useful for switching active quest
                table.insert(questsKeys, questTitle)

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Saved "' .. tostring(questTitle) .. '" quest with progress: ' .. tostring(questMatch) .. '.')
            end
        end

        -- if active quest isn't selected or deleted - select first found one
        if not quests[selectedQuest] then
            sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Updating active quest.')

            selectedQuest = questsKeys[1]
            selectedQuestIndex = 1

            sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') New active quest selected: ' .. tostring(selectedQuest) .. '.')
        end

        -- release pending dialog
        targetDialog = -1
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialogue capture released.')
        return
    end

    -- if current dialog is 'My Statistics'..
    if id == 235 then
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Prepating dialog content.')
        local preparedContent = split(content, '\n')
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialog content prepared.')

        -- run through all lines of dialog
        for _, value in ipairs(preparedContent) do
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Proccessing value: ' .. tostring(value) .. '.')

            -- remove colors from text
            value = stripColors(value)
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Stripped value: ' .. tostring(value) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching levelMatch.')
            local levelMatch = value:match('Уровень: %[(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') levelMatch: ' .. tostring(levelMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching respectMatch.')
            local respectMatch = value:match('Уважение: %[(%d+/%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') respectMatch: ' .. tostring(respectMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching bankMoneyMatch.')
            local bankMoneyMatch = value:match('Деньги в банке: %[%$(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') bankMoneyMatch: ' .. tostring(bankMoneyMatch) .. '.')

            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Matching depositeMoneyMatch.')
            local depositeMoneyMatch = value:match('Деньги на депозите: %[%$(%d+)]')
            -- sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') depositeMoneyMatch: ' .. tostring(depositeMoneyMatch) .. '.')

            -- if we find a level - save it
            if levelMatch then
                local level = tonumber(levelMatch)
                playerData.level = level

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found level: ' .. tostring(level) .. '.')
            end

            -- if we find a respect - save it
            if respectMatch then
                local exp = tonumber(respectMatch:match('%d+'))
                local nextExp = tonumber(respectMatch:match('/(%d+)'))

                playerData.exp = exp
                playerData.nextExp = nextExp

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found respect: ' .. tostring(exp) .. '/' .. tostring(nextExp) .. '.')
            end

            -- if we find a bank money - save it
            if bankMoneyMatch then
                local money = tonumber(bankMoneyMatch)
                playerData.bankMoney = money

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found bank money: ' .. tostring(money) .. '.')
            end

            -- if we find a deposite money - save it
            if depositeMoneyMatch then
                local money = tonumber(depositeMoneyMatch)
                playerData.depositeMoney = money

                sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Found deposite money: ' .. tostring(money) .. '.')
            end
        end

        -- release pending dialog
        targetDialog = -1
        sendDebug('[proccessTargetDialog] (' .. tostring(id) .. ') Dialogue capture released.')
        return
    end
end


--- render interface with player data
function renderInterface()
    local windowWidth = getScreenResolution()
    local overlay = gui.GetBackgroundDrawList()

    -- quests background
    overlay:AddRectFilled(
        gui.ImVec2(windowWidth - 600, 97),
        gui.ImVec2(windowWidth - 395, 167),
        0xE6090909,
        16, 1 + 4
    )

    -- level
    overlay:AddText(
       gui.ImVec2(windowWidth - 600, 21.5),
       0xFFA5A6A5,
       'LVL:'
    )
    overlay:AddText(
       gui.ImVec2(windowWidth - 566, 21.5),
       0xFFFFFFFF,
       tostring(playerData.level)
    )

    -- exp
    overlay:AddText(
       gui.ImVec2(windowWidth - 600, 49.5),
       0xFFA5A6A5,
       'EXP:'
    )
    overlay:AddText(
       gui.ImVec2(windowWidth - 566, 49.5),
       0xFFFFFFFF,
       tostring(playerData.exp) .. '/' .. tostring(playerData.nextExp)
    )

    -- quests
    if selectedQuest then
        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 112),
            0xFFFFFFFF,
            u8(overlap(tostring(selectedQuest)))
        )

        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 132),
            0xFFFFFFFF,
            u8(overlap(tostring(quests[selectedQuest])))
        )
    else
        overlay:AddText(
            gui.ImVec2(windowWidth - 580, 122),
            0xFFFFFFFF,
            u8'Загрузка квестов..'
        )
    end


    gui.PushFont(font['icon'])

    -- bank money
    overlay:AddText(
       gui.ImVec2(windowWidth - 240, 225),
       0xFF68FF65,
       icons['BUILDING_COLUMNS']
    )

    -- deposite money
    overlay:AddText(
       gui.ImVec2(windowWidth - 242, 265),
       0xFF68FF65,
       icons['PIGGY_BANK']
    )

    gui.PopFont()


    gui.PushFont(font[30])

    -- bank money
    overlay:AddText(
       gui.ImVec2(windowWidth - 210, 225),
       0xFFFFFFFF,
       tostring(formatMoney(tostring(playerData.bankMoney)))
    )

    -- deposite money
    overlay:AddText(
       gui.ImVec2(windowWidth - 210, 265),
       0xFFFFFFFF,
       tostring(formatMoney(tostring(playerData.depositeMoney)))
    )

    gui.PopFont()
end


--- sends script-style message to player chat
function sendMessage(message)
    sampAddChatMessage('{FFA500}' .. tostring(message), 0xFFA500)
end

--- sends debug message to console if debug is enabled
function sendDebug(message)
    if not debug then return end
    print(tostring(message))
end


--- separates string by delimiter
function split(str, delimiter)
    local result = {}

    for match in (str .. delimiter):gmatch('(.-)' .. delimiter) do
        table.insert(result, match)
    end

    return result
end

--- shortens string if it is longer than 20 characters
function overlap(text)
    if #text <= 20 then
        return text
    end

    -- get first 20 characters and add ..
    return text:sub(1, 20) .. '..'
end

--- сonverts number into readable monetary format
function formatMoney(amount)
    return tostring(amount)
        :reverse()
        :gsub("%d%d%d", "%1 ")
        :reverse()
        :gsub("^%s+", "")
end

--- removes SAMP colors from text
function stripColors(text)
    return text:gsub('{%x+}', '')
end

Логи moonloader с дебагом ( нас интересует в основном самое начало и конец ): https://pastebin.com/Cu8X9LKX

Отчёт краша: https://crash.sr.team/?uid=52e1786e-46482b32-56e669f9-79dcefad-dae3f8a8-7f24385d-ef42c033-999e6ed5

Скриншот краша:
Посмотреть вложение 218249
ето вообще под самп тебе ии написал ето?
 

wojciech?

Известный
206
120
а не легче просто принт юзать?
там стоит проверка на debug, чтобы эти сообщения появлялись не постоянно, а только когда оно того требует, хоть это и сомнительное решение
 

NyashMyash99

Известный
Автор темы
36
49
там стоит проверка на debug, чтобы эти сообщения появлялись не постоянно, а только когда оно того требует, хоть это и сомнительное решение
А почему сомнительное то? Если планируется распространять скрипт, проще попросить человека включить debug mode и отправить логи, чем добавлять print на каждую предполагаемую строчку и кидать ему файл. Или я чего-то не знаю и есть получше способ?
 

plalkeo

Известный
521
189
А почему сомнительное то? Если планируется распространять скрипт, проще попросить человека включить debug mode и отправить логи, чем добавлять print на каждую предполагаемую строчку и кидать ему файл. Или я чего-то не знаю и есть получше способ?
можно отдельным файликом хранить лог скрипта
 
  • Нравится
Реакции: NyashMyash99