Всё, что здесь лежит, написал ИИ-агент на живом сервере под запись — без ESX, без QBCore, без скачанных с форума скриптов. - resources/justrp — свой ресурс на Lua: регистрация, вход, создание персонажа, спавн - resources/justrp/html — экраны NUI: кинематографичная загрузка, вход, редактор персонажа - api/api.py — внутренний HTTP-API к MariaDB, пароли через scrypt - server.cfg.example — пример конфига Секреты (пароль БД, внутренний ключ API) вынесены в переменные окружения и convar. Ключ Cfx.re в репозиторий не попадает. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
7.9 KiB
Lua
211 lines
7.9 KiB
Lua
-- JustRP — Server-side core
|
||
-- All DB operations go through the internal Python API on :8787
|
||
|
||
local API_BASE = "http://127.0.0.1:8787"
|
||
-- Должен совпадать с JUSTRP_API_SECRET у Python-API. Задаётся в server.cfg:
|
||
-- set justrp_api_key "ваш-ключ"
|
||
local API_KEY = GetConvar("justrp_api_key", "change-me")
|
||
|
||
-- Per-player session state
|
||
local Sessions = {}
|
||
|
||
-- ─── HTTP helper ────────────────────────────────────────────────────────────
|
||
|
||
local function apiRequest(method, path, data, cb)
|
||
PerformHttpRequest(
|
||
API_BASE .. path,
|
||
function(status, body, _headers)
|
||
local ok, result = pcall(json.decode, body or "{}")
|
||
if ok then
|
||
cb(status, result)
|
||
else
|
||
cb(status, { success = false, error = "parse error" })
|
||
end
|
||
end,
|
||
method,
|
||
data and json.encode(data) or "",
|
||
{
|
||
["Content-Type"] = "application/json",
|
||
["X-API-Key"] = API_KEY,
|
||
}
|
||
)
|
||
end
|
||
|
||
-- ─── Player connecting ───────────────────────────────────────────────────────
|
||
|
||
AddEventHandler("playerConnecting", function(name, setKickReason, deferrals)
|
||
local src = source
|
||
local license = GetPlayerIdentifierByType(src, "license") or
|
||
GetPlayerIdentifierByType(src, "license2") or
|
||
"license:" .. tostring(src)
|
||
deferrals.defer()
|
||
Wait(0)
|
||
deferrals.update("Checking account…")
|
||
|
||
apiRequest("GET", "/account?license=" .. license, nil, function(status, res)
|
||
if status == 200 and res.success then
|
||
if res.account.banned == 1 then
|
||
deferrals.done("You are banned from JustRP.\nReason: " .. (res.account.ban_reason or "No reason given"))
|
||
else
|
||
deferrals.done()
|
||
end
|
||
else
|
||
-- No account yet — let them through, they'll register in-game
|
||
deferrals.done()
|
||
end
|
||
end)
|
||
end)
|
||
|
||
-- ─── Client ready → start session ───────────────────────────────────────────
|
||
|
||
RegisterNetEvent("justrp:clientReady")
|
||
AddEventHandler("justrp:clientReady", function()
|
||
local src = source
|
||
local license = GetPlayerIdentifierByType(src, "license") or
|
||
GetPlayerIdentifierByType(src, "license2") or
|
||
"license:" .. tostring(src)
|
||
Sessions[src] = { license = license, accountId = nil, charId = nil }
|
||
TriggerClientEvent("justrp:startSession", src)
|
||
end)
|
||
|
||
-- ─── Auth events ────────────────────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("justrp:auth:register")
|
||
AddEventHandler("justrp:auth:register", function(username, password)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses then return end
|
||
|
||
apiRequest("POST", "/auth/register", {
|
||
license = ses.license,
|
||
username = username,
|
||
password = password,
|
||
}, function(status, res)
|
||
if res.success then
|
||
ses.accountId = res.account_id
|
||
TriggerClientEvent("justrp:auth:ok", src, res.account_id, res.username)
|
||
else
|
||
TriggerClientEvent("justrp:auth:fail", src, res.error or "Unknown error")
|
||
end
|
||
end)
|
||
end)
|
||
|
||
RegisterNetEvent("justrp:auth:login")
|
||
AddEventHandler("justrp:auth:login", function(username, password)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses then return end
|
||
|
||
apiRequest("POST", "/auth/login", {
|
||
license = ses.license,
|
||
username = username,
|
||
password = password,
|
||
}, function(status, res)
|
||
if res.success then
|
||
ses.accountId = res.account_id
|
||
TriggerClientEvent("justrp:auth:ok", src, res.account_id, res.username)
|
||
else
|
||
TriggerClientEvent("justrp:auth:fail", src, res.error or "Unknown error")
|
||
end
|
||
end)
|
||
end)
|
||
|
||
-- ─── Character events ────────────────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("justrp:chars:load")
|
||
AddEventHandler("justrp:chars:load", function()
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses or not ses.accountId then
|
||
TriggerClientEvent("justrp:chars:list", src, {})
|
||
return
|
||
end
|
||
|
||
apiRequest("GET", "/characters?account_id=" .. ses.accountId, nil, function(status, res)
|
||
if res.success then
|
||
TriggerClientEvent("justrp:chars:list", src, res.characters or {})
|
||
else
|
||
TriggerClientEvent("justrp:chars:list", src, {})
|
||
end
|
||
end)
|
||
end)
|
||
|
||
RegisterNetEvent("justrp:char:create")
|
||
AddEventHandler("justrp:char:create", function(data)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses or not ses.accountId then return end
|
||
|
||
data.account_id = ses.accountId
|
||
apiRequest("POST", "/character/create", data, function(status, res)
|
||
if res.success then
|
||
ses.charId = res.character_id
|
||
TriggerClientEvent("justrp:char:created", src, res.character_id)
|
||
else
|
||
TriggerClientEvent("justrp:char:createFail", src, res.error or "Unknown error")
|
||
end
|
||
end)
|
||
end)
|
||
|
||
RegisterNetEvent("justrp:char:select")
|
||
AddEventHandler("justrp:char:select", function(charId)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses or not ses.accountId then return end
|
||
|
||
apiRequest("GET", "/character?id=" .. tostring(charId), nil, function(status, res)
|
||
if res.success and res.character then
|
||
local ch = res.character
|
||
if ch.account_id ~= ses.accountId then
|
||
return -- character doesn't belong to this account
|
||
end
|
||
ses.charId = charId
|
||
TriggerClientEvent("justrp:char:loaded", src, ch)
|
||
end
|
||
end)
|
||
end)
|
||
|
||
-- ─── Spawn ───────────────────────────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("justrp:spawn:request")
|
||
AddEventHandler("justrp:spawn:request", function(spawnKey)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses or not ses.charId then return end
|
||
|
||
local spawns = {
|
||
legion = { x = -166.7, y = -928.9, z = 31.4, h = 120.0 },
|
||
vespucci = { x = -1380.0, y = -1520.0, z = 4.9, h = 75.0 },
|
||
sandy = { x = 1843.4, y = 3683.0, z = 34.3, h = 200.0 },
|
||
paleto = { x = -165.8, y = 6330.0, z = 31.5, h = 270.0 },
|
||
airport = { x = -1037.0, y = -2738.0, z = 20.0, h = 330.0 },
|
||
}
|
||
local pos = spawns[spawnKey] or spawns["legion"]
|
||
TriggerClientEvent("justrp:spawn:go", src, pos)
|
||
end)
|
||
|
||
-- ─── Auto-save position every 60s ────────────────────────────────────────────
|
||
|
||
RegisterNetEvent("justrp:savePosition")
|
||
AddEventHandler("justrp:savePosition", function(x, y, z, h)
|
||
local src = source
|
||
local ses = Sessions[src]
|
||
if not ses or not ses.charId then return end
|
||
apiRequest("POST", "/character/save", {
|
||
character_id = ses.charId,
|
||
last_x = x,
|
||
last_y = y,
|
||
last_z = z,
|
||
last_heading = h,
|
||
}, function() end)
|
||
end)
|
||
|
||
-- ─── Disconnect ──────────────────────────────────────────────────────────────
|
||
|
||
AddEventHandler("playerDropped", function()
|
||
local src = source
|
||
Sessions[src] = nil
|
||
end)
|
||
|
||
print("[JustRP] Server core loaded.")
|