BTYN

A linguagem .btyn

← Índice

Um schema é um arquivo de texto puro. Comentários seguem Luau: -- até o fim da linha e --[[ ]] para bloco. Não existem palavras reservadas — from, rate e event são reconhecidos por posição, então um campo pode se chamar qualquer um deles.

Declarações

event

Confiável, ordenado, agrupado uma vez por frame.

event Attack from client {
    target: entity,
    combo:  u8,
}

from é obrigatório e diz quem envia. Ele decide qual metade da API cada lado recebe: um evento from client.fire ao cliente e .on ao servidor. Chamar o lado errado é erro de tipo, não um silêncio.

unreliable

Mesmo formato, enviado por UnreliableRemoteEvent. Pode chegar fora de ordem ou simplesmente não chegar.

unreliable Muzzle from server {
    at:  vec3,
    dir: unit,
}

Use quando perder um pacote é invisível: fogo de boca, poeira de passo, atualização de mira que a próxima substitui de qualquer jeito. Nunca para nada que tenha consequência.

O compilador calcula o pior caso de tamanho de todo pacote unreliable e quebra o build se ele não couber no limite de payload. Isso importa porque o comportamento do motor com um payload unreliable grande demais é descartar em silêncio.

error: unreliable event 'Snapshot' can reach 1003 bytes, over the 800 byte cap
  --> net.btyn:12:12
   |
12 | unreliable Snapshot from server {
   |            ^^^^^^^^ worst case is 1003 bytes
   |
help: the engine drops an oversized unreliable payload without telling you.
      Shrink the packet (quantise floats, cap arrays tighter), split it, or
      make it a reliable `event`

request

Requisição e resposta pelos mesmos dois remotes. Nenhum RemoteFunction está envolvido — RemoteFunction bloqueia quem chamou e, no servidor, uma invocação que dá yield trava a replicação atrás dela.

request Buy from client rate 4 {
    item:     u16,
    quantity: u8(1..99),
} -> {
    ok:      bool,
    balance: u32,
    reason:  string(64),
}

Quem pede recebe .call (dá yield, lança em falha) e .try (devolve ok, value). Quem responde recebe .on, e o handler pode dar yield à vontade — ele roda na própria thread, então uma chamada de datastore não trava o batch.

Toda requisição tem prazo (request_timeout, padrão 10s). Uma resposta que nunca chega falha a chamada em vez de deixar a thread presa para sempre.

channel

Estado replicado, codificado em delta, com audiência explícita. Sempre servidor → cliente, então não aceita from.

channel Health priority high {
    hp:     u8(0..100),
    armor:  u8,
    downed: bool,
}

Só os campos que mudaram são enviados, atrás de uma bitmask de sujos. Um tick de vida custa um byte de máscara mais um byte de valor, não o registro inteiro.

priority é low, normal (padrão) ou high. Quando um cliente passa do orçamento de bytes do frame, low é adiado primeiro e high nunca é adiado. Deltas adiados se fundem com a próxima atualização, então nada se perde — só atrasa.

Limitado a 32 campos, o que mantém a máscara em um único acesso. Dividir é melhor design de qualquer forma: campos que mudam em ritmos diferentes querem prioridades diferentes.

Um channel não aceita rate nem from. Ele já envia no máximo uma vez por frame por jogador, e sempre replica do servidor para o cliente; os dois são rejeitados em tempo de compilação em vez de aceitos e ignorados.

struct e enum

struct Vec2i { x: i16, y: i16 }

enum Team { Red, Blue, Spectator }

Uma struct é organizada recursivamente e empacota os próprios booleanos internamente. Um enum viaja em um byte (dois acima de 256 variantes) e aparece no Luau como união de strings — "Red" | "Blue" | "Spectator" — para o código continuar legível.

config

config {
    server = "src/Server/Net.luau"
    client = "src/Client/Net.luau"
}
Chave Padrão Significado
server obrigatório Onde escrever o módulo do servidor
client obrigatório Onde escrever o módulo do cliente
runtime game:GetService("ReplicatedStorage"):WaitForChild("BTYN") Expressão Luau que os arquivos gerados usam para achar o runtime
budget 40000 Orçamento suave de saída por cliente, bytes/segundo
unreliable_cap 800 Maior payload unreliable, em bytes
max_packets_per_batch 256 Máximo de pacotes num batch recebido, no servidor
request_timeout 10 Segundos até uma requisição sem resposta falhar
write_checks true Validar na saída além da entrada
manual false Dar flush na mão em vez de no Heartbeat

Sobre unreliable_cap: a Roblox não publica esse número, e medição da comunidade coloca ele perto de 1000 bytes. O padrão fica bem abaixo disso de propósito — errar para baixo custa alguns bytes por pacote, errar para cima custa perda silenciosa de dados. Meça no seu próprio place antes de aumentar.

Sobre write_checks: o lado que recebe sempre valida, porque ali é a fronteira de confiança. Essa flag controla só o lado que envia, onde o custo compra pegar hp = 300 no ponto da chamada em vez de vê-lo truncar em silêncio para 44 e chegar do outro lado como um mistério. Deixe ligado até um profile dizer o contrário.

Sobre max_packets_per_batch: o menor pacote no fio é um único byte de opcode, então sem um teto um payload de um cliente hostil vira dezenas de milhares de chamadas de handler num frame — uma amplificação que os limites de rate por pacote não enxergam, porque só são alcançados depois que o trabalho já foi feito. O padrão fica muito acima do jogo normal, onde um frame movimentado de input são alguns poucos pacotes. Vale para o que o servidor recebe e nunca para os batches do próprio servidor, que legitimamente são longos: um keyframe de channel por entidade replicada é um pacote cada.

Sobre manual: com ele ligado, nada é enviado sozinho e o Net.flush() fica por sua conta. O orçamento de bytes por frame dos channels acompanha o tempo real decorrido em vez de assumir sessenta flushes por segundo, então uma cadência diferente ainda gasta budget bytes por segundo em vez de sessenta vezes mais.

Tipos

Números

u8 u16 u32 · i8 i16 i32 · f32 f64

Todos aceitam uma faixa inclusiva opcional, validada nos dois lados:

hp:   u8(0..100),
temp: i16(-50..50),

Uma faixa que não cabe no tipo é erro de compilação.

bool

Custa um bit, não um byte. Todo booleano do pacote entra em um bitfield na frente, escrito com um único acesso por 32 flags.

entity

Um u32 cujo significado é seu. Existe com nome próprio para te afastar de Instance, que não cabe em buffer, custa banda de verdade, e chega nil quando quem recebe ainda não carregou o objeto.

string(n)

O limite é obrigatório. Uma string sem limite é um pacote sem limite, o que é vazamento de banda e primitiva de amplificação de graça. O prefixo de tamanho é um byte até 255, dois acima disso.

[T; n]

Array com limite obrigatório, mesma razão. [u16; 100] tem no máximo 201 bytes.

T?

Opcional. Quando T tem tamanho fixo isso custa um bit e mantém o pacote no caminho rápido de offset constante — os bytes do payload são sempre escritos, zerados quando ausente. Quando T é dinâmico, custa um byte de presença.

Esse bit de presença vive no bitfield do pacote (ou do struct), que elementos de array e campos de channel não têm — então um opcional de tipo fixo é rejeitado nesses dois lugares. Embrulhe num struct, que carrega seu próprio bitfield: struct Slot { value: u16? }. Opcionais de tipos dinâmicos gastam um byte de presença e funcionam em qualquer lugar.

Valores da Roblox

Tipo Bytes Observação
vec3 12 3 × f32
cframe 18 posição em 3 × f32, rotação em ângulos de Euler quantizados
color3 3 um byte por canal
unit 6 vetor unitário, cada componente quantizado em i16; normalizado na leitura
angle 2 radianos quantizados em [-pi, pi]
Instance 2 índice em um array lateral — veja abaixo

fixed(min, max, bytes)

Um float quantizado numa faixa fixa. bytes é 1, 2 ou 4.

blend: fixed(-1, 1, 2),

Dois bytes dão 65536 passos na faixa, mais fino do que qualquer jogador percebe. Normalmente é a resposta certa quando você ia usar f32 no reflexo.

Instance

Buffer guarda bytes, então uma Instance não cabe. O BTYN escreve um índice e leva a referência ao lado do payload.

Funciona, e ainda assim prefira entity. Referência de Instance custa banda e chega nil quando quem recebe ainda não fez streaming do objeto — um bug que só aparece em conexão ruim. Um broadcast com Instance também abre mão do caminho de serializar uma vez só, porque os índices são relativos ao batch que os carrega.


Próximo: a API gerada · compilador e editor · performance · segurança