Depuração

From Multi Theft Auto: Wiki

É comum ao programar cometer erros de sintaxe e semântica. O primeiro deles acontece porque o computador não tem a sensibilidade do ser humano para compreender um texto, mesmo que tenha erros de ortografia. Já o segundo, é basicamente culpa sua, não do computador. Por exemplo, em um complexo algoritmo para distribuir jogadores pelo mapa, ocorre uma divisão por zero. De fato, até este ponto, a máquina só seguiu suas instruções, que eventualmente, em um caso específico chegou a uma operação matemática inválida.

A depuração (debug, em inglês) ajuda o programador a encontrar erros apontando o que são eles são e onde estão, poupando seu valioso tempo. Esta página irá ensinar como ativar a depuração e dar alguns exemplos de como corrigir erros comuns.

Debug no Console

O MTA vem com um sistema no console onde mensagens de debug relacionadas às funções e scripts são mostradas. Lembrando que é necessário ter uma permissão de administrador para ativá-lo (a não ser se você modificar o ACL). Para fazer isso, digite debugscript X no console, em que X é o nível:

  • 1: mostra somente os erros
  • 2: erros e avisos
  • 3: erros, avisos e mensagens informativas

Ao digitar debugscript 3, todas as ocorrências serão mostradas. O nível 3 ou o 2 são os recomendados para a maioria dos casos. É recomendado ativar o debugscript' para testar seu script e detectar os erros de digitação.

Exemplo

Neste código há dois erros. Consegue encontra-los?

function SayHello(message, player)
    if (getPlayerName(player) == "Cebola")
        outputChatbox("Olá, Cebola!")
    end
end
addEventHandler("onChatMessage", root, SayHello)

Quando este script for carregado, no console irá aparecer algo assim:

INFO: Loading script failed: meuRecurso\script.lua:2: 'then' expected near ´outputChatbox'


Isso significa que o script não pode ser executado porque há um erro de sintaxe. Nesta mensagem, primeiramente aparece qual script de qual recurso deu problema. Depois disso, há dois pontos e um número mostrando a linha onde está o erro; para facilitar o trabalho do programador quando se trata de scripts maiores. Depois disso, a mensagem irá variar de acordo com o tipo de erro. Neste caso, é fácil concluir que o erro está localizado no meuRecurso e na linha dois do arquivo script.lua. Desta vez está na cara: precisamos colocar o "then" na cláusula if.

function SayHello(message, player)
    if (getPlayerName(player) == "Cebola") then
        outputChatbox("Olá, Cebola!")
    end
end
addEventHandler("onChatMessage", root, SayHello)

Agora tudo será carregado com sucesso e nenhum erro aparecerá até que um jogador apelidado "Cebola" dizer algo no chat. Aí no console irá aparecer:

ERROR: myResource\script.lua:2: attempt to call global 'outputChatbox' (a nil value)

Isso significa que a função outputChatbox é um valor nil, ou seja, ele não existe! Isso porque a função na verdade se chama outputChatBox, e não outputChatbox. Então, tome cuidado com a letra maiúscula B.:

function SayHello(message, player)
    if (getPlayerName(player) == "Cebola") then
        outputChatBox("Olá, Cebola!")
    end
end
addEventHandler("onChatMessage", root, SayHello)

Lembrando que isso é só um exemplo e não será sempre desse jeito, pois há vários tipos de erros que podem acontecer.

Log de debug do Cliente e Servidor

Servidor

Na pasta ../server/mods/deathmatch, haverá dois arquivos com funções parecidas:

  • local.conf - são as configurações oriundas do item "host game" no menu principal do MTA. Isto é uma maneira rápida de iniciar um servidor de testes dentro do próprio programa. Quando o usuário fecha o jogo, ele é automaticamente desligado também.
  • mtaserver.conf - é usado quando se inicia o "MTA Server.exe" dentro da pasta ../server/. Em outras palavras, o servidor é rodado em prompt de comando e não necessita de o MTA estar executando junto. Isso é muito útil para os interessados em montar um servidor dedicado ou mesmo experimentar como é mexer em um servidor de verdade.


Dependendo das suas necessidades, com certeza irá querer alterar essas configurações abaixo:

	<!-- Indica o nome e a localização do arquivo usado pelo debugscript. Se for deixado em branco, o servidor não o salvará. -->
	<scriptdebuglogfile>logs/scripts.log</scriptdebuglogfile> 
	
	<!-- Indica o nível de debugscript para este arquivo. Todos os níveis 0,1,2,3 estão disponíveis. Quando não especificado, zero é o padrão. -->
	<scriptdebugloglevel>0</scriptdebugloglevel>

Como as descrições acima já dizem, é opcional salvar um arquivo de log de acordo com o nível desejado. Os três níveis estão descritos acima, sendo neste caso o zero como a não ativação do debugscript - fazendo o arquivo ficar em branco. Se 3 for ativado, todos os erros encontrados no lado do servidor serão salvos em: ../server/mods/deathmatch/logs/scripts.log

Cliente

Todos os erros do cliente serão salvos na pasta ../server/clientscript.log. Isso já é definido por padrão, então nenhuma configuração é necessária.

Habilidades em Deubug

Em vez de ficar procurando no código os erros cometidos, há algumas coisas a serem feitas para ajudar a encontrar-los. A maioria delas estão relacionadas com a adição de alguma mensagem de debug informando o que está ocorrendo no script.

Funções Úteis

As funções abaixo podem lhe ajudar com o debug:

  • outputDebugString ou outputChatBox para escrever qualquer informação na tela (de preferência outputDebugString para suporte técnico)
  • tostring() para tornar qualquer variável em uma string. Muito útil para saber o que ela é, escrevendo na tela. Não será necessário usa-la se a variável já uma string ou um número.
  • getElementType verifica o tipo de elemento do MTA em questão.
  • isElement verifica se elemento existe.

Adicionando mensagens de debug para verificar se, quando ou por qual frequência um código é executado

Se você terminou de escrever um código e percebe que algo esperado não acontece e fica em dúvida se as instruções foram executadas ou não; nesse caso é recomendado adicionar mensagens de debug para verificar os passos.
Um exemplo típico seria checar se uma clausula if foi atendida ou não:

if (variable1 == variable2) then
	outputDebugString("A variável 1 é igual a 2.")
	-- portanto, faça alguma coisa
end

Outra maneira semelhante seria verificar se alguma variável muda. Em outras palavras, é só adicionar uma mensagem de debug depois de cada vez que isso acontece.

Adicionando uma mensagem de debug para verificar o valor de uma variável

Digamos que desejas criar um marcador (marker), mas ele não aparece na posição esperada. A primeira coisa a ser feita é verificar se a função createMarker foi executada com sucesso. Depois disso, só precisamos verificar os valores usados nela.

local marker = createMarker(x,y,z)
	if not marker then
		outputChatBox("O marcador não pode ser criado.")
	end
outputChatBox("A posX é "..x..", posY "..y..", e posZ "..z)

Feito isso, as variáveis usadas como coordenadas serão mostradas. Lembrando que se uma variável não for uma frase ou número, é recomendado usar a função tostring(), pois ela torna qualquer variável agrupável em uma string, mesmo se for uma bool ou tabela.

Exemplo

Imagine que você criou um detector de colisão e se o jogador ficar dez segundos dentro dele, algo vai acontecer:

function colShapeHit(player)
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit", root, colShapeHit)

function colShapeLeave(player)
	-- destrua o timer quando o jogador sair do colshape
	killTimer(colshapeTimer[player])
end
addEventHandler("onColShapeLeave", root, colShapeLeave)

Quando o jogador entrar no colshape, o debugscript irá escrever a seguinte mensagem:

ERROR: ..[path]: attempt to index global 'colshapeTimer' (a nil value)

Isso significa que você tentou endereçar uma tabela não existente quando tentou colocar o timer dentro dela; ou seja, ela é um valor nil - portanto, não existe. Para corrigir isso, precisamos verificar se a tabela não existe, e caso a resposta for positiva; a criaremos.

function colShapeHit(player)
	if (colshapeTimer == nil) then
		colshapeTimer = {}
	end
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit", root, colShapeHit)

function colShapeLeave(player)
	-- destrua o timer quando o jogador sair do colshape
	killTimer(colshapeTimer[player])
end
addEventHandler("onColShapeLeave",root,colShapeLeave)

Desta vez receberemos um aviso quando o jogador entrar no colshape, esperar a mensagem e sair de lá:

WARNING: [..]: Bad argument @ 'killTimer' Line: ..

Tirando isso (vamos corrigir mais tarde!), tudo parece funcionar bem. O jogador entra no colshape e o timer começa a contar: se ele esperar, a mensagem é escrita, caso contrário o timer é destruído. Mas...

Um erro imperceptível

Por alguma razão, quando o jogador entra no colshape usando um veículo, a mensagem é escrita duas vezes; como se o código tivesse sido executado duas vezes. Então, adicionamos uma mensagem de debug para verificar isso.

function colShapeHit(player)
	if (colshapeTimer == nil) then
		colshapeTimer = {}
	end
	-- adicionamos uma mensagem de debug
	outputDebugString("colShapeHit")
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit",getRootElement(),colShapeHit)

function colShapeLeave(player)
	-- adicionamos outra mensagem de debug
	outputDebugString("colShapeLeave")
	-- destrua o timer quando o jogador sair do colshape
	killTimer(colshapeTimer[player])
end
addEventHandler("onColShapeLeave",getRootElement(),colShapeLeave)

Agora dá pra ter certeza que as funções são executadas duas vezes quando entramos com um veículo, mas só uma quando se está a pé. É possível que o veículo seja um elemento a parte e ativa a função de forma independente ao jogador. Para confirmar essa teoria, verificamos se a variável player realmente se refere ao jogador.

function colShapeHit(player)
	if (colshapeTimer == nil) then
		colshapeTimer = {}
	end
	-- adicionamos uma mensagem de debug, seguido do elemento "jogador"
	outputDebugString("colShapeHit"..getElementType(player))
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit",getRootElement(),colShapeHit)

function colShapeLeave(player)
	-- adicionamos outra mensagem de debug, incluindo o jogador
	outputDebugString("colShapeLeave "..getElementType(player))
	-- destrua o timer quando o jogador sair do colshape
	killTimer(colshapeTimer[player])
end
addEventHandler("onColShapeLeave",getRootElement(),colShapeLeave)

A mensagem de debug nos diz que uma referência da variável player é realmente o jogador, mas outra é o veículo. Já que nós destinamos o evento especificamente ao boneco dele, adicionaremos uma cláusula if, obrigando o término da execução do código caso a variável não for o jogador.

function colShapeHit(player)
	if (colshapeTimer == nil) then
		colshapeTimer = {}
	end
	-- adicionamos um verificador
	if (getElementType(player) ~= "player") then return end
	-- adicionamos uma mensagem de debug, seguido do elemento "jogador"
	outputDebugString("colShapeHit"..getElementType(player))
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit",getRootElement(),colShapeHit)

function colShapeLeave(player)
	-- adicionamos ele novamente
	if (getElementType(player) ~= "player") then return end
	-- adicionamos outra mensagem de debug, incluindo o jogador
	outputDebugString("colShapeLeave "..getElementType(player))
	-- destrua o timer quando o jogador sair do colshape
	killTimer(colshapeTimer[player])
end
addEventHandler("onColShapeLeave",getRootElement(),colShapeLeave)

Agora tudo está funcionando como o planejado, mas aquela mensagem de aviso mencionada acima ainda aparece. Isso porque quando tentamos destruir o timer depois que o jogador saí do colshape, ele não existe mais, já que os 10 segundos se passaram. Em outras palavras, o timer é automaticamente destruído quando ele conta os 10 segundos. Há diversas formas de se livrar do aviso, uma vez que temos conhecimento da não existência do timer. Uma delas seria verificar se ele existe na tabela. Para fazer isto, usamos a função isTimer na hora de destruí-lo:

if (isTimer(colshapeTimer[player])) then
	killTimer(colshapeTimer[player])
end

Então o código completo ficará assim:

function colShapeHit(player)
	if (colshapeTimer == nil) then
		colshapeTimer = {}
	end
	-- adicionamos um verificador
	if (getElementType(player) ~= "player") then return end
	-- adicionamos uma mensagem de debug, seguido do elemento "jogador"
	outputDebugString("colShapeHit"..getElementType(player))
	-- criamos um timer para escrever uma mensagem (pode também ativar uma função)
	-- e inserimos ele em uma tabela, usando o jogador como index
	colshapeTimer[player] = setTimer(outputChatBox,10000,1,"O jogador permaneceu dez segundos dentro do colshape!")
end
addEventHandler("onColShapeHit",getRootElement(),colShapeHit)

function colShapeLeave(player)
	-- adicionamos ele novamente
	if (getElementType(player) ~= "player") then return end
	-- adicionamos outra mensagem de debug, incluindo o jogador
	outputDebugString("colShapeLeave "..getElementType(player))
	-- destrua o timer quando o jogador sair do colshape
	if (isTimer(colshapeTimer[player])) then
		killTimer(colshapeTimer[player])
	end
end
addEventHandler("onColShapeLeave",getRootElement(),colShapeLeave)

Verificando problemas na performance

Se o seu servidor estiver trabalhando muito além da conta ou você deseja se certificar que seus scripts são eficientes, é possível matar o problema pela raiz usando uma boa ferramenta inclusa na instalação: performancebrowser. É só inicia-lo usando 'start performancebrowser'. Caso ele não exista, pode encontrá-lo no último pacote de recursos hospedado no GitHub. Esta ferramenta inclui uma série de informações relativas a performance, como:

  • Memory leaks - uso excessivo de memória
  • Element leaks - criação excessiva de elementos
  • CPU usage - uso excessivo de CPU

Para acessar o performancebrowser, é preciso ir até o navegador e digitar o endereço: http://<ip-do-servidor>:<porta>/performancebrowser/. Um exemplo seria o endereço do servidor local: http://127.0.0.1:22005/performancebrowser/. Lembrando que é preciso:

  • colocar a barra (/) no final do endereço
  • logar no jogo com uma conta de administrador ou outra que tenha acesso no ACL a:
  • resource.performancebrowser.http e resource.ajax.http


[[{{{image}}}|link=]] Vale Lembrar: A maioria das informações relevantes estão nas categorias Lua timing e Lua memory. E, o problema está nos valores mais altos apresentados.


Exemplos de scripts problemáticos

Um deles seria adicionar dados a alguma tabela, mas não remover posteriormente. Porém, isso levaria alguns meses ou até anos para danificar o funcionamento do servidor.

local someData = {}

function storeData()
	someData[source] = true
	-- não há nenhum evento para retirar os dados quando o jogador sair. Isso é considerado memory leak
	-- Entrando na aba Lua timing, você pode verificar o uso de RAM de cada recurso.
end
addEventHandler("onPlayerJoin", root, storeData)

Um caso possível de Element leak é criar colisores temporários e nunca destruí-los mais tarde. Isso aumenta muito o uso de transferência dados (bandwidth), CPU e memória pelo servidor.

function useTemporaryCol()
	local col = createColCircle(some code here)
	if (algo que deveria acontecer...) then
		destroyElement(col)
	end
	-- E as vezes não acontece, fazendo o colisor permanecer no mapa pra sempre...
	-- Criando de centenas à milhares de coliores sem função alguma.
	-- Na aba Lua timing é possível ver quantos elementos cada script criou.
end

O alto uso de CPU resulta na queda de FPS, fazendo o servidor um lugar ruim para se jogar. Em 24 horas, isso pode causar sérios prejuízos. O número de elementos criados detectados na aba Lua timing não ajudará neste caso, mas o Lua memory sim.

addEventHandler("onPlayerJoin", root, function()
	-- [Código para o novo jogador]
	-- Até que um evento é adicionado para quando ele sair
	addEventHandler("onPlayerQuit", root, function()
	-- Encontrou o problema? Esse evento além de usar o root como o seu causador, é criado infinitamente...
    end)
end)

Uma função usa muito o CPU quando ela demora para terminar de ser executada. O exemplo abaixo é uma delas. Sem o performancebrowser, você não perceberia tão cedo que esta é a causa do problema. Mas com ele, verá na aba Lua timing que o seu recurso usa muito o potencial do CPU. Ficará mais fácil descobrir isso se usar o parâmetro 'd' na caixa de entrada Options. Isso porque ela lhe dirá também a linha em que está o código problemático.

function umaFuncaoPoucoEficaz()
	-- para cada cem mil coisas quaisquer
	for i=1, 100000 do
		-- [código abaixo]
	end
end