Denver Studio - Docs

Coletores

O que cada arquivo de collectors faz e em qual intervalo

Coletores

O script é dividido em 6 coletores independentes em server/collectors/. Cada um tem sua própria responsabilidade e cadência de push.

Resumo dos intervalos

ColetorCadênciaTipoEndpoint
push_metrics.luaConfig.IntervalSec (60s padrão)Snapshot agregado/v1/ingest/push-metrics
orgs.luaA cada ~5 minSnapshot por permissão/v1/ingest/org-members
money.luaA cada ~10 minSnapshot por jogador/v1/ingest/player-money
players.luaEvent-driven1 evento por sessão/v1/ingest/player-session-start /v1/ingest/player-session-end /v1/ingest/player-job-change
heatmap.luaSnapshot 30 min + eventos imediatosPosições + drops/v1/ingest/positions /v1/ingest/disconnect-batch
transactions.luaFlush 30s (batch até 500)Eventos de entrada/saída de dinheiro/v1/ingest/player-transactions

push_metrics.lua

O coração do script. Roda a cada Config.IntervalSec (padrão 60s) e envia um snapshot consolidado:

{
  "cfxId": "abc123",
  "players": 200,
  "maxPlayers": 1000,
  "entries": 12,         // novos players desde último push
  "exits": 8,
  "newPlayers": 2,
  "bans": 0,
  "legalOnline": 35,
  "illegalOnline": 41,
  "groups": {
    "legal":   { "Polícia": 12, "Paramédico": 8, "Mecânica": 15 },
    "ilegal":  { "Drogas": 20, "Contrabando": 21 }
  },
  "staffMembers": [
    { "externalId": "1234", "name": "João", "role": "Admin" }
  ]
}

Alimenta: Visão Geral, Métricas, Legal & Ilegal (counts), Banimentos, Staff online.

Inicia automaticamente 2 segundos após o resource subir (via server/main.lua).


players.lua

Event-driven. Não faz push em intervalo — reage a eventos:

EventoAção
vRP:playerSpawn / ConnectEnvia player-session-start com license, nome, cfxId, cohortTag
vRP:playerLeave / Disconnect / playerDroppedEnvia player-session-end com durationMs, lastJob
vRP:playerJoinGroupEnvia player-job-change com fromJob, toJob, jobType

Alimenta: página Retenção — cohorts, churn risk, time-to-first-job legal vs ilegal.

Mantém um mapa local sessionStart[license] = os.time() para calcular duração corretamente.


orgs.lua

Push periódico (loop com Wait(INTERVAL_MS), inicia 15s após o resource subir).

Para cada permissão em Config.LegalOrganizations + Config.IllegalOrganizations, chama Adapter.getOrgMembers(permission) e envia:

{
  "cfxId": "abc123",
  "groupName": "Polícia",
  "groupType": "legal",
  "fullSnapshot": true,
  "members": [
    {
      "passport": "1234",
      "name": "João",
      "lastName": "Silva",
      "license": "abc",
      "lastLoginAt": 1700000000,
      "level": 5
    }
  ]
}

Alimenta: páginas Facções e Grupos do painel — lista de membros, hierarquia, último login.

Mesmo se o player estiver offline, o Adapter.getOrgMembers retorna ele — porque os dados vêm do banco (characters table), não da memória.


money.lua

Push periódico (loop, inicia 30s após o resource subir).

Chama Adapter.getPlayersMoney() que retorna todos os personagens que logaram nos últimos 7 dias com cash + bank:

{
  "cfxId": "abc123",
  "players": [
    { "passport": "1234", "name": "João", "cash": 5000, "bank": 250000 }
  ]
}

Alimenta: página Economia — M2 em circulação, inflação semanal, top milionários, distribuição (Gini), detector de duping via z-score.

Este coletor faz uma query MySQL pesada (JOIN com playerdata para extrair cash via JSON_VALUE). Por isso a cadência é longa (10+ minutos). Se o seu DB for pequeno (< 50k characters) você pode reduzir, mas evite cadências menores que 5 min.


heatmap.lua

Dois threads paralelos:

Thread 1 — Snapshot de posições (30min)

A cada 30 minutos:

  1. Itera todos os players online via GetPlayers()
  2. Para cada um, lê GetEntityCoords(GetPlayerPed(src)) (server-side, OneSync)
  3. Captura passport + jobType via adapter
  4. Adiciona ao batch pendingPositions
Wait(0)  -- yield entre cada player para não travar

Thread 2 — Eventos de disconnect (imediato)

No handler playerDropped(reason):

  1. Lê a posição final do ped (ainda válido por alguns ms após o drop)
  2. Classifica reason em: crash / drop_s2c / drop_c2s / quit
  3. Adiciona ao batch pendingDisconnects

Thread 3 — Flush (30s)

A cada 30 segundos:

  • Se há disconnects pendentes → 1 POST batch para /v1/ingest/disconnect-batch
  • Se há snapshots pendentes → 1 POST para /v1/ingest/positions

Alimenta: página Mapa de calor — concentração de jogadores, locais de crash, drops S→C e C→S.

Requer OneSync. Sem OneSync, GetEntityCoords server-side retorna (0, 0, 0) para todos, e o mapa fica vazio.


transactions.lua

Buffer em memória + flush a cada 30 segundos em batches de até 500 entradas. Buffer máximo de 5000 entradas (entries acima disso são descartadas pra não estourar memória).

Não dispara sozinho — depende de chamadas dos exports addIncome e addExpense feitas a partir do seu sistema de banco/loja/recompensas. Veja Exports → Transações e Hooks → Transações.

{
  "cfxId": "abc123",
  "batch": [
    {
      "passport": "1234",
      "kind": "income",
      "amount": 5000,
      "description": "Salário Polícia",
      "category": "salario",
      "timestamp": 1700000000000
    },
    {
      "passport": "1234",
      "kind": "expense",
      "amount": 250,
      "description": "Compra mercado 24h",
      "category": "loja",
      "timestamp": 1700000005000
    }
  ]
}

Alimenta: aba Transações da página Economia — log por jogador, filtro por período, soma por categoria, detecção de movimentos suspeitos.

Faz flush automático no onResourceStop pra não perder eventos pendentes durante restart do resource.


Classificação de drop reason

Regras aplicadas em classifyReason(reason):

Padrão (lowercase)kind
disconnected. exiting kickedquit
timed out time-out connection lostdrop_c2s
reliable network event overflow onesync server overloadeddrop_s2c
crash failed to set up game crashed couldn't loadcrash
(default)crash