Scripting Lua

Filtro de texto: o jogo sai do ar até você adicionar, e a responsabilidade é sua

A Roblox filtra o chat sozinha. Todo o resto do texto que o jogo mostra é responsabilidade de quem faz — e jogo detectado sem filtro é removido até o filtro existir.

por Gustavo Cantino9 min de leituraIntermediário

Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

Placa tipográfica com a categoria Scripting Lua em destaque, o nível intermediário abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

Existe uma frase na documentação de filtro de texto que muda o peso do assunto inteiro, e ela não está escrita como aviso: se a Roblox receber denúncias ou detectar automaticamente que o seu jogo não aplica filtro de texto, o sistema remove o jogo até você adicionar o filtro.

Não é advertência, não é redução de alcance. É o jogo fora do ar. Este guia cobre o que precisa ser filtrado, como se filtra, e os erros que deixam buraco.

O problema

O que o filtro faz: ele impede que o jogador veja linguagem imprópria e informação pessoal, como número de telefone. Os dois importam, e o segundo costuma ser esquecido — o filtro não existe só contra palavrão.

A divisão de responsabilidade é onde quase todo mundo erra, e ela cabe em duas frases. A Roblox filtra automaticamente as saídas comuns, como mensagem que passou pelo chat do jogo. E a responsabilidade por filtrar qualquer texto exibido que você não controla explicitamente é sua.

O engano é achar que "o Roblox filtra" vale para tudo. Vale para o chat. O resto — placa, nome de bicho, apelido, mural, qualquer lugar onde o jogador escreve e outra pessoa lê — é do jogo.

E a documentação lista quatro cenários, que juntos cobrem praticamente todo caso:

  • Entrada direta do jogador: por TextBox, por uma interface de teclado feita com botões, ou por um teclado interativo no espaço 3D.
  • Texto gerado: um jogo que gera palavras a partir de caracteres aleatórios e as mostra. É o cenário que ninguém prevê — há chance de criar palavra imprópria por acaso, e a chance não é pequena em escala.
  • Conteúdo externo: um jogo que busca conteúdo de um servidor externo para exibir dentro do jogo. A documentação explica por que isso é perigoso mesmo sendo o seu servidor: é comum não se ter controle do conteúdo, e um terceiro pode editar a informação.
  • Texto guardado: texto em DataStore — nome de bicho é o exemplo dado — que pode conter palavra imprópria e precisa ser filtrado na leitura.

Esse último merece atenção porque ele é retroativo. Todo texto que os seus jogadores salvaram antes de você implementar o filtro continua lá, e vai aparecer na tela na próxima vez que alguém carregar aquele save.

Passo a passo

1. Colete no cliente, filtre no servidor. O exemplo oficial coleta a entrada no evento FocusLost e manda ao servidor por um RemoteEvent. No servidor, o texto passa primeiro pelo FilterStringAsync e depois pelo método de transmissão.

2. Chame o FilterStringAsync. Ele recebe o texto e o UserId de quem escreveu. O UserId não é formalidade: é ele que diz ao filtro de quem é o texto.

3. Escolha o método certo do resultado. A chamada devolve um objeto TextFilterResult, com dois métodos adicionais, e a escolha entre eles é sobre quem vai ler:

  • Um deles é para texto visível a todos os jogadores do servidor — o caso da placa que todo mundo enxerga.
  • O outro exibe o texto filtrado para um jogador específico, levando em conta idade e outros detalhes dele.

Essa diferença é real e não é cosmética. O filtro por usuário é mais restritivo ou mais permissivo dependendo de quem está lendo; o de transmissão precisa servir para todos ao mesmo tempo, e por isso aperta mais.

4. Filtre depois do envio, não durante. A documentação é explícita: não filtre em tempo real, a cada caractere digitado no TextBox. O motivo técnico é que isso produz texto visível só para quem está digitando. A orientação é filtrar depois que o jogador envia.

5. Trate a falha como ausência de texto. Este passo não está na documentação como passo, mas é o que decide se a implementação sobrevive a um dia ruim de rede. A próxima seção detalha.

Vale ainda entender por que os dois métodos existem em vez de um. O filtro não é uma lista fixa de palavras proibidas: ele é sensível a quem lê. O que um adulto vê e o que uma criança vê não são a mesma coisa, e a plataforma sabe a diferença. Quando o texto vai para uma pessoa só, dá para aplicar o critério dela; quando vai para o servidor inteiro, o filtro precisa servir para o leitor mais restrito que possa estar ali.

Daí sai uma consequência de desenho: texto público é mais filtrado que texto privado. Se a sua placa aparece censurada demais, a resposta não é trocar de método — é aceitar que texto que todo mundo lê tem um padrão mais apertado, e que isso é o comportamento pedido.

Artefato

O módulo abaixo expõe os dois métodos com nomes que dizem a decisão — paraTodos e paraUm — e traz os três usos no rodapé.

A regra que vale copiar mesmo que o resto não sirva está no comentário do meio: na falha, não mostre o original.

O filtro é uma chamada de rede, e chamada de rede falha. A tentação, ao tratar o erro, é cair para o texto que o jogador escreveu — afinal, é melhor mostrar alguma coisa. Essa linha transforma uma instabilidade momentânea em texto não filtrado na tela, que é exatamente a condição que tira o jogo do ar. Todas as funções do artefato devolvem nil na falha, e o chamador trata nil como "não tenho texto para mostrar".

No caso da placa, isso significa manter o texto anterior. Texto velho é melhor que texto cru.

Erro comum

Filtrar no cliente. O erro mais frequente e o mais inútil. O filtro roda no servidor porque o cliente não é confiável — um cliente modificado simplesmente não chama a função, e o texto chega ao servidor cru. Filtrar no cliente dá a sensação de proteção e não protege nada.

Filtrar a cada tecla. Contra a orientação explícita, e além disso custa uma chamada de rede por caractere. O FocusLost existe para isso.

Esquecer o texto guardado. É o buraco mais comum em jogo que já existe. Você implementa o filtro na entrada, testa, funciona — e todo o texto salvo antes continua saindo sem filtro na leitura. A correção é filtrar na leitura também, o que parece redundante e não é: o dado antigo nunca passou por filtro nenhum.

Confiar no próprio servidor externo. Se o jogo busca texto de uma API sua, é natural pensar que aquilo é conteúdo controlado. A documentação desmonta isso: um terceiro pode editar a informação. E mesmo sem terceiro, um erro seu na API vira texto não filtrado no jogo — sem filtro na borda, a sua API passa a ser parte da superfície de moderação do jogo.

Passar o UserId errado. O UserId é de quem escreveu, não de quem está lendo. No caso do nome de bicho lido de um DataStore, quem escreveu foi o dono do save, possivelmente há meses, e pode nem estar no servidor. Passar o UserId do leitor muda o que o filtro considera.

Não validar antes de filtrar. O filtro não é validação: ele não confere tamanho, não confere tipo, não limita frequência. Uma string de um milhão de caracteres chega ao FilterStringAsync e vira uma chamada de rede cara. Validar tipo, tamanho e taxa vem antes.

Um cuidado adicional vale para jogo que já está no ar: implementar o filtro não é o fim do trabalho, porque a superfície cresce sozinha. Todo recurso novo que aceite texto — um campo de apelido, uma caixa de mensagem, um sistema de clã com nomes — nasce sem filtro a menos que alguém lembre. A defesa que sobrevive é ter uma única porta por onde todo texto de jogador passa, como o módulo do artefato, em vez de repetir a chamada em cada lugar.

Próximo passo

O exercício concreto de hoje é uma varredura, e ela é curta: liste todo lugar do seu jogo onde um texto que veio de fora aparece na tela. Vale texto digitado, texto gerado, texto lido de banco e texto buscado na web.

Para cada item da lista, responda duas coisas: ele passa pelo filtro? E o filtro roda no servidor?

A lista costuma ser mais longa do que a memória sugere. O TextBox óbvio está lá, mas também o nome do time que alguém escolheu, o apelido no placar, o texto do mural, a descrição do item que o jogador nomeou. Cada um deles é um lugar onde o jogo mostra a uma pessoa algo que outra escreveu — que é a definição do que precisa ser filtrado.

E, se o jogo tem texto salvo de antes, vale programar a leitura filtrada como prioridade. Não há como saber o que está guardado até alguém carregar — e quando carregar, já está na tela.

Fontes

Como corrigimos erros

Artefato pronto pra usar

Filtrar texto de jogador, com os dois métodos e o caso do DataStore (ServerScriptService)
lua
-- ServerScriptService: o único lugar do jogo que filtra texto.
--
-- A escolha entre os dois métodos NÃO é de gosto -- ela é sobre QUEM VAI
-- LER, e errar significa ou mostrar o que não devia, ou censurar o que
-- não precisava:
--
--   ParaTodos   -> o texto aparece para o servidor inteiro. Placa,
--                  nome de bicho, mensagem no mural.
--   ParaUm      -> o texto aparece para UMA pessoa, e o filtro leva em
--                  conta a idade e outros detalhes DELA.

local TextService = game:GetService("TextService")

local Filtro = {}

-- Passo comum: FilterStringAsync com o texto e o UserId de QUEM ESCREVEU.
-- O UserId não é enfeite: é ele que diz ao filtro de quem é o texto.
local function resultado(texto, deUserId)
	local r
	local ok, erro = pcall(function()
		r = TextService:FilterStringAsync(texto, deUserId)
	end)
	if not ok then
		warn("FilterStringAsync falhou: " .. tostring(erro))
		return nil
	end
	return r
end

--[[
  A REGRA QUE SALVA O JOGO: na falha, NÃO MOSTRE O ORIGINAL.

  O filtro é uma chamada de rede e pode falhar. A tentação é "se falhou,
  mostra o que o jogador escreveu" -- e essa linha transforma uma falha
  de rede em texto não filtrado na tela, que é exatamente o que faz o
  jogo ser removido. Todas as funções abaixo devolvem nil na falha, e o
  chamador precisa tratar nil como "não tenho texto para mostrar".
]]

function Filtro.paraTodos(texto, deUserId)
	if type(texto) ~= "string" or texto == "" then
		return nil
	end
	local r = resultado(texto, deUserId)
	if not r then
		return nil
	end
	local ok, filtrado = pcall(function()
		return r:GetNonChatStringForBroadcastAsync()
	end)
	return ok and filtrado or nil
end

function Filtro.paraUm(texto, deUserId, paraUserId)
	if type(texto) ~= "string" or texto == "" then
		return nil
	end
	local r = resultado(texto, deUserId)
	if not r then
		return nil
	end
	local ok, filtrado = pcall(function()
		return r:GetNonChatStringForUserAsync(paraUserId)
	end)
	return ok and filtrado or nil
end

--[[
═══════════════════════════════════════════════════════════════════════
OS TRÊS USOS
═══════════════════════════════════════════════════════════════════════

-- 1. ENTRADA DE TEXTO. O cliente manda no FocusLost; o servidor filtra.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("TextoDaPlaca")
local placa = workspace:WaitForChild("Placa").SurfaceGui.TextLabel

remote.OnServerEvent:Connect(function(player, texto)
    -- validar ANTES de filtrar: string, tamanho, taxa
    if type(texto) ~= "string" or #texto == 0 or #texto > 200 then return end

    local limpo = Filtro.paraTodos(texto, player.UserId)
    if limpo then
        placa.Text = limpo
    end
    -- se limpo for nil, a placa NÃO muda. Melhor texto velho que texto cru.
end)

-- 2. TEXTO VINDO DO DATASTORE. Salvo antes da regra, ou salvo por uma
--    versão antiga do jogo: filtre na LEITURA, não confie no que está lá.
local function nomeDoBicho(player, nomeSalvo, donoUserId)
    return Filtro.paraUm(nomeSalvo, donoUserId, player.UserId)
end

-- 3. CONTEÚDO DE SERVIDOR EXTERNO. Você não controla o que vem, e um
--    terceiro pode editar a fonte a qualquer momento. Filtre igual.
--    O UserId aqui é o de quem RECEBE, na falta de um autor conhecido.
local function textoExterno(player, vindoDaWeb)
    return Filtro.paraUm(vindoDaWeb, player.UserId, player.UserId)
end
]]

return Filtro
Neste artigo

Continue por aqui