Licenças como infraestrutura Roleplay
Um sistema de licenças e autorizações é uma infraestrutura fundamental para qualquer servidor de roleplay sério. Sem ele, cada jogador pode fazer tudo desde o primeiro dia. As licenças restringem as atividades a requisitos realistas, criando progressão, consequências e interações de roleplay. Uma carteira de motorista significa que os jogadores devem passar por um teste antes de operar veículos legalmente. Uma licença de armas requer verificação de antecedentes e treinamento. Uma licença de caça restringe quem pode caçar e onde. Uma licença comercial controla quem pode operar empreendimentos comerciais. Uma licença de pesca, uma licença de piloto, uma licença médica, uma licença legal – cada uma cria uma camada de regulamentação que reflecte os sistemas do mundo real e gera interacções orgânicas entre cidadãos, funcionários públicos e autoridades policiais. Quando um policial para alguém e verifica o status de sua licença, esse é um momento de dramatização que só existe porque o sistema de licença torna isso possível. Este tutorial percorre a construção de uma estrutura de licença flexível e extensível que oferece suporte a qualquer tipo de licença que seu servidor precise.
Configuração flexível do tipo de licença
Em vez de codificar tipos de licença específicos, crie um sistema baseado em configuração onde a adição de um novo tipo de licença exija apenas uma nova entrada em uma tabela de configuração. Cada definição de tipo de licença inclui um identificador exclusivo, nome de exibição, descrição, custo de inscrição, se é necessário um exame, as questões do exame, se aplicável, a duração da validade antes da renovação ser necessária, quais empregos ou funções podem emiti-la e quais restrições de jogo se aplicam quando a licença está faltando. Essa abordagem significa que sua carteira de caça, carteira de motorista, licença de porte de arma e licença comercial funcionam todas no mesmo mecanismo subjacente. Quando seu servidor cresce e precisa de um novo tipo de licença, como um medalhão de táxi ou uma licença de construção, você adiciona uma entrada de configuração e todo o pipeline de aplicação, emissão e fiscalização funciona automaticamente sem escrever novo código.
-- shared/config.lua
Config = {}
Config.LicenseTypes = {
driving = {
label = 'Driving License',
description = 'Required to legally operate motor vehicles',
cost = 500,
requiresExam = true,
examType = 'practical',
validDays = 365,
issuedBy = {'dmv', 'police'},
restrictions = {'vehicle_operation'},
},
weapons = {
label = 'Weapons Permit',
description = 'Required to legally carry firearms',
cost = 2500,
requiresExam = true,
examType = 'written',
validDays = 180,
issuedBy = {'police'},
restrictions = {'weapon_carry'},
prerequisites = {'driving'},
},
hunting = {
label = 'Hunting License',
description = 'Required for legal hunting activities',
cost = 750,
requiresExam = true,
examType = 'written',
validDays = 90,
issuedBy = {'ranger', 'dmv'},
restrictions = {'hunting_activity'},
},
fishing = {
label = 'Fishing Permit',
description = 'Required for legal fishing activities',
cost = 200,
requiresExam = false,
validDays = 30,
issuedBy = {'dmv', 'ranger'},
restrictions = {'fishing_activity'},
},
business = {
label = 'Business License',
description = 'Required to operate a commercial business',
cost = 5000,
requiresExam = false,
validDays = 365,
issuedBy = {'government'},
restrictions = {'business_operation'},
},
pilot = {
label = 'Pilot License',
description = 'Required to operate aircraft',
cost = 10000,
requiresExam = true,
examType = 'practical',
validDays = 180,
issuedBy = {'faa'},
restrictions = {'aircraft_operation'},
prerequisites = {'driving'},
},
}Fluxo de inscrição e exame
O processo de inscrição deve parecer uma verdadeira interação governamental. Os jogadores visitam um local do DMV, correio ou prédio governamental relevante e interagem com um NPC ou quiosque para iniciar o aplicativo. A NUI apresenta os tipos de licença disponíveis, seus custos e requisitos. Após selecionar uma licença e pagar a taxa, o sistema verifica os pré-requisitos. Se a licença exigir um exame escrito, o jogador entra em uma interface de questionário com questões de múltipla escolha retiradas de um conjunto de questões configuráveis. Randomize a ordem das perguntas e as posições das respostas para evitar fraudes na memorização. Exija uma pontuação mínima para aprovação, normalmente de 70 a 80 por cento. Para exames práticos, como testes de direção, crie um veículo e crie uma rota de checkpoint que o jogador deve completar dentro de um limite de tempo, seguindo as leis de trânsito. Um NPC instrutor de direção ou um jogador real trabalhando no DMV pode supervisionar o teste e reprovar os alunos que ultrapassam o sinal vermelho, aceleram excessivamente ou batem. Armazene o resultado do exame e, caso seja aprovado, emita a licença com validade a partir do carimbo de data/hora atual.
-- server/licenses.lua
function ApplyForLicense(playerId, licenseType)
local config = Config.LicenseTypes[licenseType]
if not config then return false, 'Invalid license type' end
local identifier = GetPlayerIdentifier(playerId, 0)
-- Check prerequisites
if config.prerequisites then
for _, prereq in ipairs(config.prerequisites) do
local has = HasValidLicense(identifier, prereq)
if not has then
return false, 'Missing prerequisite: ' ..
Config.LicenseTypes[prereq].label
end
end
end
-- Check if already has active license
local existing = MySQL.single.await([[
SELECT id, status, expires_at FROM licenses
WHERE identifier = ? AND license_type = ?
AND status IN ('active','suspended')
]], {identifier, licenseType})
if existing and existing.status == 'active' then
return false, 'You already hold this license'
end
-- Deduct application fee
local paid = exports['framework']:RemoveMoney(
playerId, config.cost, 'bank')
if not paid then
return false, 'Insufficient funds ($' .. config.cost .. ' required)'
end
-- Create pending application
local appId = MySQL.insert.await([[
INSERT INTO license_applications
(identifier, license_type, status, applied_at)
VALUES (?, ?, 'pending', NOW())
]], {identifier, licenseType})
if config.requiresExam then
return true, 'Application submitted. Please complete the exam.', appId
else
-- No exam required, issue directly
IssueLicense(identifier, licenseType)
return true, 'License issued successfully!'
end
end
function IssueLicense(identifier, licenseType)
local config = Config.LicenseTypes[licenseType]
local expiresAt = os.time() + (config.validDays * 86400)
MySQL.insert([[
INSERT INTO licenses
(identifier, license_type, status, issued_at, expires_at)
VALUES (?, ?, 'active', NOW(), FROM_UNIXTIME(?))
ON DUPLICATE KEY UPDATE
status = 'active', issued_at = NOW(),
expires_at = FROM_UNIXTIME(?)
]], {identifier, licenseType, expiresAt, expiresAt})
end
function HasValidLicense(identifier, licenseType)
local result = MySQL.single.await([[
SELECT id FROM licenses
WHERE identifier = ? AND license_type = ?
AND status = 'active' AND expires_at > NOW()
]], {identifier, licenseType})
return result ~= nil
end
exports('HasValidLicense', HasValidLicense)Integração da aplicação da lei
O verdadeiro poder de um sistema de licenças surge quando as autoridades policiais podem consultar e modificar os status das licenças durante as interações de roleplay. Os policiais precisam ter a capacidade de verificar o status da licença de um cidadão durante as batidas de trânsito, verificar as licenças de armas durante as buscas e suspender ou revogar as licenças como consequência do comportamento criminoso. Crie uma integração policial MDT que mostre todas as licenças detidas por um cidadão, juntamente com seu status, data de emissão e data de validade. Adicione comandos ou ações MDT para suspender uma licença com motivo e duração, revogá-la permanentemente e restabelecê-la após um período de suspensão. Quando uma licença é suspensa, o titular deverá receber uma notificação explicando o motivo e a duração da suspensão. Integre verificações de licenças em fluxos de trabalho de fiscalização existentes. Se um jogador for pego dirigindo sem carteira válida, o sistema policial pode sinalizá-lo automaticamente durante uma parada de trânsito. Se alguém for pego caçando sem licença, os guardas florestais podem emitir citações que acarretam multas e potencial revogação da licença.
-- server/enforcement.lua
RegisterNetEvent('licenses:checkCitizen', function(targetId)
local src = source
-- Verify requesting player is law enforcement
local job = exports['framework']:GetPlayerJob(src)
if job ~= 'police' and job ~= 'ranger' and job ~= 'sheriff' then
return
end
local targetIdentifier = GetPlayerIdentifier(targetId, 0)
local licenses = MySQL.query.await([[
SELECT license_type, status, issued_at, expires_at,
suspended_reason, suspended_until
FROM licenses WHERE identifier = ?
]], {targetIdentifier})
TriggerClientEvent('licenses:showResults', src, licenses)
end)
RegisterNetEvent('licenses:suspend', function(targetIdentifier, licType, reason, days)
local src = source
local job = exports['framework']:GetPlayerJob(src)
if job ~= 'police' and job ~= 'judge' then return end
local suspendUntil = os.time() + (days * 86400)
MySQL.update([[
UPDATE licenses SET
status = 'suspended',
suspended_reason = ?,
suspended_until = FROM_UNIXTIME(?)
WHERE identifier = ? AND license_type = ?
]], {reason, suspendUntil, targetIdentifier, licType})
-- Notify the affected player if online
local targetPlayer = GetPlayerFromIdentifier(targetIdentifier)
if targetPlayer then
TriggerClientEvent('licenses:notify', targetPlayer,
'Your ' .. Config.LicenseTypes[licType].label ..
' has been suspended for ' .. days .. ' days. Reason: ' .. reason)
end
-- Log the action
MySQL.insert([[
INSERT INTO license_logs
(identifier, license_type, action, performed_by, reason)
VALUES (?, ?, 'suspend', ?, ?)
]], {targetIdentifier, licType, GetPlayerIdentifier(src, 0), reason})
end)Sistema de renovação e tratamento de expiração
Licenças com datas de expiração criam receitas recorrentes e interações regulares com NPCs que mantêm o mundo vivo. Quando uma licença se aproxima da data de expiração, envie ao titular uma notificação por meio do sistema telefônico ou do sistema de correio abordado em um tutorial separado. Dê um período de carência de alguns dias no jogo em que a licença está tecnicamente expirada, mas o jogador não é penalizado imediatamente, permitindo tempo para renovação. A renovação deve ser mais simples do que o pedido inicial, exigindo apenas a taxa e nenhum reexame, a menos que a licença tenha sido previamente suspensa ou revogada. Implemente uma tarefa agendada no servidor que seja executada periodicamente para verificar licenças expiradas e atualizar seu status. Para licenças que exigem renovação periódica, como licenças de pesca com períodos de validade curtos, considere oferecer opções de renovação em massa, onde os jogadores possam pagar antecipadamente por vários períodos com um pequeno desconto. Isso recompensa jogadores dedicados que planejam com antecedência enquanto mantêm o ciclo de renovação que mantém o sistema dinâmico.
Aplicação de restrições em scripts
O sistema de licença só importa se outros scripts realmente aplicarem as restrições. Crie uma função de exportação centralizada que qualquer script possa chamar para verificar se um jogador possui uma licença válida de um determinado tipo. O script do seu veículo verifica se há uma carteira de motorista antes de permitir a partida do motor ou quando um jogador entra no assento do motorista. Seu script de armas verifica uma licença de porte de arma quando um jogador equipa uma arma de fogo. Seu script de caça verifica uma licença de caça antes de permitir a colheita de animais. Seu roteiro de pesca verifica se há uma licença de pesca antes de permitir as capturas. Cada script decide suas próprias consequências para as violações. Alguns podem simplesmente impedir a ação com uma notificação. Outros podem permitir a ação, mas sinalizar o jogador para atenção das autoridades, acionando um nível ou alerta automático de procurado. O rigor depende da filosofia de roleplay do seu servidor. O sistema de licença fornece a camada de dados; os scripts de aplicação decidem as consequências. Essa separação significa que você pode ajustar o rigor da aplicação sem mexer no próprio sistema de licenças.
Esquema de banco de dados e ferramentas administrativas
O esquema do banco de dados precisa de tabelas para definições de licença já em configuração, licenças ativas, aplicativos, resultados de exames e um log de auditoria. O registro de auditoria é fundamental para a prestação de contas. Cada emissão, suspensão, revogação e reintegração de licença é registrada com um carimbo de data/hora, o identificador do oficial executor e o motivo. Os comandos administrativos devem permitir que a equipe emita qualquer licença diretamente para fins de evento, elimine todas as suspensões durante eventos de anistia, expire em massa licenças de um tipo específico para reinicializações do sistema e visualize um histórico completo de licenças para qualquer jogador. Crie um painel de administração no painel da web ou MDT do seu servidor que forneça estatísticas rápidas sobre licenças ativas por tipo, aplicativos pendentes, suspensões recentes e expirações futuras. Esses dados ajudam você a entender como os jogadores interagem com o sistema e se as taxas e os períodos de validade precisam de ajustes para manter um envolvimento saudável.
-- SQL schema
CREATE TABLE IF NOT EXISTS licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
status ENUM('active','suspended','revoked','expired')
DEFAULT 'active',
issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
suspended_reason VARCHAR(255) DEFAULT NULL,
suspended_until TIMESTAMP NULL,
UNIQUE KEY unique_license (identifier, license_type),
INDEX idx_status (status),
INDEX idx_expires (expires_at)
);
CREATE TABLE IF NOT EXISTS license_applications (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
status ENUM('pending','passed','failed','cancelled')
DEFAULT 'pending',
exam_score INT DEFAULT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL
);
CREATE TABLE IF NOT EXISTS license_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL,
license_type VARCHAR(32) NOT NULL,
action VARCHAR(32) NOT NULL,
performed_by VARCHAR(64),
reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_identifier (identifier)
);
