SAML: reconstruindo o futebol competitivo do Roblox do zero
Como e por que estou reescrevendo o padrão MPS com uma camada de rede binária própria, física customizada e um sistema de balanceamento centralizado.

Meu codinome no Roblox é Jonuffy, e SAML (South American MPS League) é o projeto que mais me ensinou sobre arquitetura de sistema nos últimos anos. Não é um estúdio grande, não é um jogo AAA. É um framework de futebol competitivo construído sozinho, e por isso mesmo cada decisão de design custa caro: não tem equipe para absorver o erro depois.
Este texto é sobre o que existe por trás da bola indo de um jogador para outro em tempo real. Rede, física, balanceamento e as falhas que uma auditoria de segurança encontrou no caminho.
De onde o gênero veio
Futebol competitivo no Roblox nasceu em 2008 com o TPS (Tayfun Professional Soccer), um conjunto simples de cinco ferramentas: chute, passe, lançamento, carrinho, drible. A comunidade pegou essa base e reescreveu, dando origem ao MPS (Modified Professional Soccer). Não um jogo específico, um padrão. Qualquer criador podia pegar um campo MPS aberto e construir a própria liga em cima dele.
O maior sucesso desse padrão foi o PRS (Professional Roblox Soccer), fundado em 2015. Uma década de atualizações incrementais o transformou na referência do gênero, e também acumulou uma dívida técnica pesada. Módulos com require() recursivo, dependência de APIs externas que derrubavam o servidor inteiro quando saíam do ar, um sistema de posse de bola difícil de manter. A resposta da comunidade foi reescrever o PRS do zero, sob o codinome Project Vulpes.
SAML nasceu de um impulso parecido, mas de um ponto de partida diferente. Em vez de reformar um sistema existente, decidi construir a fundação de novo, pensando primeiro em arquitetura e só depois em feature.
O que muda por baixo do capô
Rede. Em vez de um RemoteEvent por tipo de mensagem, que é o padrão mais comum em jogos Roblox e uma fonte constante de acoplamento, SAML usa só dois: um para mensagens confiáveis, outro para não confiáveis. Todo o tráfego passa por esses dois canais, serializado num buffer binário compacto em formato TLV (tipo, tamanho, valor). A parte que mais rende é o batching automático: toda chamada de rede feita dentro de um frame é enfileirada e despachada de uma vez no evento PostSimulation. Isso derruba o número de disparos de rede por frame, exatamente onde a replicação intensa de física e animação costuma engasgar.
Mecânicas como dados. O StarterPack separa quatro responsabilidades que normalmente vivem misturadas em um jogo Roblox comum. Mechanics guarda estado: posse de bola, perna selecionada, barra de força. Rig cuida da física visível: ragdoll, welds animáveis. Runners são máquinas de estado para cada movimento, implementadas com task.delay. Controls liga tudo isso a input e HUD. Na prática, adicionar um movimento novo é editar uma tabela de dados no Playbook, sem tocar em lógica central.
Um lugar só para números mágicos. Cooldowns e durações não ficam espalhados pelo código. Vivem num módulo Balance central, organizados por categoria de movimento: drible, passe, chute, goleiro. O Playbook referencia a categoria e o valor certo é injetado em runtime. É a técnica mais simples do framework inteiro e provavelmente a que mais economiza tempo de manutenção.
Física própria. Eventos de toque nativos do Roblox são lentos demais para detectar contato com um objeto de alta velocidade como uma bola de futebol. Por isso implementei detecção de colisão própria, calculando ponto de contato com precisão suficiente para chutes e defesas.
Posse sob latência. O ponto mais delicado de qualquer jogo competitivo em rede é decidir quem controla a bola quando existe ping envolvido. O servidor amostra a posição reportada pelo cliente numa janela curta, calcula o ping do jogador e rebobina o estado da bola até o instante em que ela era visível para aquele cliente, antes de decidir a posse. O sistema também registra avisos quando a discrepância entre posição reportada e posição real é grande demais, o que costuma ser sinal de exploit e não de lag comum.
O que a auditoria encontrou
Publiquei o código para revisão externa em algum momento do desenvolvimento, e a auditoria voltou com uma lista de falhas que valem como lembrete para qualquer sistema que cresce rápido demais.
| Categoria | Falha | Por que importa |
|---|---|---|
| Idempotência | ProcessReceipt não verificava se uma compra já tinha sido processada |
Roblox pode reenviar confirmação de pagamento por falha de rede, então sem checagem o item era concedido duas vezes |
| Segregação de permissão | Pontos comprados com Robux podiam pagar a remoção do próprio banimento | Misturava moeda com penalidade de moderação e criava incentivo para comprar impunidade |
| Autorização | Comandos como team, tp, clone e invisible não checavam o nível de poder de quem executava |
Abria caminho para um admin júnior rodar comando de admin sênior |
| Padrão de busca | unban e team usavam string.find sem plain=true |
Tratava entrada do cliente como padrão de busca em vez de texto literal |
| Segredo hardcoded | Webhook do Discord e UserId de admin fixos no código | Vazamento trivial se algum módulo do servidor ficasse exposto |
| Superfície de ataque | Módulos de servidor guardados em ReplicatedFirst |
Código que deveria ser só do servidor ficava potencialmente legível por um cliente com exploit de leitura |
| Validação de posse | DropBall não confirmava se quem chamou o comando era dono da bola |
Qualquer jogador podia roubar posse sem interação de jogo válida |
Duas dessas falhas não são bugs de código, são bugs de modelo de dados. A ausência de idempotência em ProcessReceipt é o erro mais comum em qualquer sistema que processa confirmação de pagamento assíncrona: a fonte vai reenviar em caso de timeout, e tratar cada notificação como evento único em vez de comando idempotente é uma escolha, não um acidente. A mistura de moeda com penalidade de moderação é o mesmo erro noutra roupa, dois domínios que deveriam ser independentes compartilhando substrato sem fronteira.
Modularizar código resolve manutenibilidade. Não resolve segurança sozinho. Isso pede verificação explícita em cada fronteira de confiança, e foi exatamente o que essa auditoria trouxe de volta para o projeto.
Onde o SAML está agora
O material que gravei quando o projeto estava na versão 0.2.2, em 02 de agosto, já está defasado. Hoje o SAML está na versão 0.31, publicada em 17 de agosto, com o placar animado refinado, replay reconstruído e o sistema de replicação de shielding estabilizado. Alguns clipes dessa evolução:
- Placar animado
- Replay com animação dinâmica
- Replicação de shielding
- Replay da replicação de shielding
- Replay de gameplay completo
Quem quiser jogar direto encontra o place publicado em SAML New Tools. Para quem quer o arquivo do jogo, o .rbxl está hospedado para download direto.
O fio comum
Vulpes escolheu pagar dívida técnica jogando fora o sistema antigo inteiro. SAML escolheu pagar construindo fronteiras melhores desde o início, e ainda assim precisou de uma auditoria para achar onde essas fronteiras vazavam. As duas conclusões apontam para o mesmo lugar: arquitetura limpa reduz o custo da dívida técnica, mas não zera ela. Isso só acontece quando alguém audita as costuras, de novo e de novo, a cada versão nova.
Continue lendo
- 5 min
A fundação técnica: Git, open source, REST, GraphQL e MySQL
O que sustenta os projetos por baixo da interface, e as regras que adotei para cada camada depois de errar em todas elas. - 3 min
Mundaú Sentinela: dado hidrológico público não é aviso de cheia
Os dados da bacia do Mundaú já existiam e eram abertos. O que faltava era alguém traduzir número de cota em decisão de quem mora na margem. - 3 min
Guardião da Praia: moderação é a parte difícil da ciência cidadã
Coletar registro de poluição costeira é trivial. Fazer com que esse registro sirva a pesquisador e a órgão ambiental é um problema de confiança, não de formulário.