Files
redl-fivem-rp/resources/[local]/fiverp-characters/client/cl_chars.lua
T
Claude Opus 5andClaude Opus 5 c857979a55 FiveRP: the server from the hosting account, not the earlier one
Wrong server was published before. This is the one that runs on the
hosting account the video was made on: FiveRP - two-step registration
(account, then an identity printed onto a passport), login that returns
you to your resident, and a loading screen driven by the game's own
streaming events.

  resources/[local]/fiverp-auth          NUI + scrypt hashes + oxmysql
  resources/[local]/fiverp-characters    identity, character load, spawn
  resources/[local]/fiverp-loadscreen    loading screen
  sql/schema.sql                         accounts, characters
  docs/DESIGN.md                         the design system every screen follows
  docs/screens/                          shot from the live server

Only our own code is in here. cfx-server-data and oxmysql are fetched by
install.sh instead of being vendored, which keeps the repo at 3.7 MB.

install.sh does the whole box in one command: recommended FXServer
build, cfx-server-data, oxmysql, MariaDB with the schema, resources,
server.cfg (mode 600 - it carries the key and the database password),
boot entry, then it waits for the Cfx registration. Tested end to end on
a spare install root: 29 resources scanned, database and schema created,
and it stopped exactly where a wrong licence key should stop it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:42:20 +00:00

371 lines
11 KiB
Lua

-- Character slots, live appearance creator and spawn.
-- The preview ped IS the player ped, so what the player sculpts is literally
-- the body they walk away with — no separate preview entity to keep in sync.
local isOpen = false
local inEditor = false
local activeChar = nil -- id of the character being played
local editSlot = nil -- slot being filled
local editId = nil -- id when finishing an existing draft
local cam = nil
local zoom = 'head'
local PREVIEW_POS = vector3(-1042.0, -2745.0, 21.36)
local PREVIEW_HDG = 315.0
local SPAWN_POS = vector3(195.17, -889.36, 30.69)
local SPAWN_HDG = 145.0
-- Camera framing per zoom step: forward distance, height above the ped root,
-- and the height it looks at.
local FRAMES = {
head = { dist = 0.85, height = 0.70, look = 0.68 },
body = { dist = 2.30, height = 0.30, look = 0.25 },
legs = { dist = 2.10, height = -0.35, look = -0.45 },
}
-- Fallback counts, used only if the native that reports them is unavailable.
local OVERLAY_FALLBACK = { 24, 29, 34, 15, 75, 7, 12, 11, 10, 18, 17, 12 }
-- ------------------------------------------------------------------- helpers
local function loadModel(name)
local hash = GetHashKey(name)
if not IsModelInCdimage(hash) then return nil end
RequestModel(hash)
local timeout = GetGameTimer() + 10000
while not HasModelLoaded(hash) and GetGameTimer() < timeout do Wait(0) end
return HasModelLoaded(hash) and hash or nil
end
local function num(v, fallback)
v = tonumber(v)
if v == nil then return fallback end
return v + 0.0
end
-- Overlays 1/2/10 take hair colours, 4/5/8 take makeup colours, the rest none.
local function overlayColourType(index)
if index == 1 or index == 2 or index == 10 then return 1 end
if index == 4 or index == 5 or index == 8 then return 2 end
return 0
end
local function applyAppearance(app)
if type(app) ~= 'table' then return end
local wanted = GetHashKey(app.model or 'mp_m_freemode_01')
if GetEntityModel(PlayerPedId()) ~= wanted then
local hash = loadModel(app.model or 'mp_m_freemode_01')
if hash then
SetPlayerModel(PlayerId(), hash)
SetModelAsNoLongerNeeded(hash)
SetPedDefaultComponentVariation(PlayerPedId())
end
end
local ped = PlayerPedId()
local b = app.blend or {}
SetPedHeadBlendData(ped,
math.floor(num(b.shapeFirst, 0)), math.floor(num(b.shapeSecond, 0)), 0,
math.floor(num(b.skinFirst, 0)), math.floor(num(b.skinSecond, 0)), 0,
num(b.shapeMix, 0.5), num(b.skinMix, 0.5), 0.0, false)
local features = app.features or {}
for i = 0, 19 do
SetPedFaceFeature(ped, i, num(features[i + 1], 0.0))
end
local overlays = app.overlays or {}
for i = 0, 11 do
local o = overlays[i + 1] or {}
local value = math.floor(num(o.v, 255))
SetPedHeadOverlay(ped, i, value, num(o.o, 1.0))
local kind = overlayColourType(i)
if kind > 0 then
SetPedHeadOverlayColor(ped, i, kind, math.floor(num(o.c1, 0)), math.floor(num(o.c2, 0)))
end
end
local hair = app.hair or {}
SetPedComponentVariation(ped, 2, math.floor(num(hair.style, 0)), 0, 2)
SetPedHairColor(ped, math.floor(num(hair.color, 0)), math.floor(num(hair.highlight, 0)))
SetPedEyeColor(ped, math.floor(num(app.eye, 0)))
local fit = app.outfit or {}
SetPedComponentVariation(ped, 11, math.floor(num(fit.torso, 0)), 0, 2) -- jacket / top
SetPedComponentVariation(ped, 8, math.floor(num(fit.undershirt, 0)), 0, 2)
SetPedComponentVariation(ped, 4, math.floor(num(fit.legs, 0)), 0, 2)
SetPedComponentVariation(ped, 6, math.floor(num(fit.shoes, 0)), 0, 2)
SetPedComponentVariation(ped, 3, 0, 0, 2) -- bare arms/torso
end
-- ------------------------------------------------------------------- camera
local function updateCamera()
if not cam or not DoesCamExist(cam) then return end
local ped = PlayerPedId()
local f = FRAMES[zoom] or FRAMES.head
local pos = GetOffsetFromEntityInWorldCoords(ped, 0.0, f.dist, f.height)
local look = GetOffsetFromEntityInWorldCoords(ped, 0.0, 0.0, f.look)
SetCamCoord(cam, pos.x, pos.y, pos.z)
PointCamAtCoord(cam, look.x, look.y, look.z)
end
local function startCamera()
if cam and DoesCamExist(cam) then DestroyCam(cam, false) end
cam = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 32.0, false, 0)
SetCamActive(cam, true)
RenderScriptCams(true, false, 0, true, true)
updateCamera()
end
local function stopCamera()
if cam and DoesCamExist(cam) then
RenderScriptCams(false, true, 700, true, true)
DestroyCam(cam, false)
end
cam = nil
end
-- Park the ped on the creator stage: visible, lit, frozen, facing the camera.
local function stagePed()
local ped = PlayerPedId()
RequestCollisionAtCoord(PREVIEW_POS.x, PREVIEW_POS.y, PREVIEW_POS.z)
SetEntityCoordsNoOffset(ped, PREVIEW_POS.x, PREVIEW_POS.y, PREVIEW_POS.z, false, false, false)
SetEntityHeading(ped, PREVIEW_HDG)
FreezeEntityPosition(ped, true)
SetEntityInvincible(ped, true)
SetEntityVisible(ped, true, false)
SetEntityCollision(ped, true, true)
ClearPedTasksImmediately(ped)
end
-- ---------------------------------------------------------------------- NUI
local function sendLimits()
local ped = PlayerPedId()
local overlays = {}
for i = 0, 11 do
local ok, count = pcall(GetNumHeadOverlayValues, i)
overlays[i + 1] = (ok and count and count > 0) and count or OVERLAY_FALLBACK[i + 1]
end
SendNUIMessage({ action = 'limits', payload = {
overlays = overlays,
hair = GetNumberOfPedDrawableVariations(ped, 2),
torso = GetNumberOfPedDrawableVariations(ped, 11),
undershirt = GetNumberOfPedDrawableVariations(ped, 8),
legs = GetNumberOfPedDrawableVariations(ped, 4),
shoes = GetNumberOfPedDrawableVariations(ped, 6),
}})
end
local function openUI(view)
isOpen = true
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open', view = view })
end
local function closeUI()
isOpen = false
SetNuiFocus(false, false)
SendNUIMessage({ action = 'close' })
end
-- Hold the player still and silent while the slots or the creator are up.
CreateThread(function()
while true do
if isOpen then
DisableAllControlActions(0)
-- Mouse look would fight the scripted camera.
DisableControlAction(0, 1, true)
DisableControlAction(0, 2, true)
SetPlayerInvincible(PlayerId(), true)
Wait(0)
else
Wait(300)
end
end
end)
-- --------------------------------------------------------------- the flow
local function beginSelection()
exports.spawnmanager:setAutoSpawn(false)
-- A clear midday sky so nobody sculpts a face in the dark.
NetworkOverrideClockTime(13, 0, 0)
SetWeatherTypeNowPersist('EXTRASUNNY')
local ped = PlayerPedId()
SetEntityVisible(ped, false, false)
stagePed()
SetEntityVisible(ped, false, false)
startCamera()
zoom = 'body'
updateCamera()
openUI('select')
TriggerServerEvent('fiverp-chars:request')
DoScreenFadeIn(600)
end
-- fiverp-auth hands over with a local TriggerEvent once the account is known.
RegisterNetEvent('fiverp-chars:begin', beginSelection)
AddEventHandler('onClientMapStart', function()
if not activeChar then exports.spawnmanager:setAutoSpawn(false) end
end)
RegisterNetEvent('fiverp-chars:slots', function(slots)
SendNUIMessage({ action = 'slots', payload = slots })
end)
RegisterNetEvent('fiverp-chars:saveResult', function(result)
SendNUIMessage({ action = 'saveResult', payload = result })
end)
local function enterCity(data)
activeChar = data.id
inEditor = false
closeUI()
DoScreenFadeOut(500)
Wait(600)
stopCamera()
NetworkClearClockTimeOverride()
ClearWeatherTypePersist()
local pos = data.position
local x, y, z, h
if pos and pos.x and pos.x ~= 0 then
x, y, z, h = pos.x + 0.0, pos.y + 0.0, pos.z + 0.0, (pos.heading or 0.0) + 0.0
else
x, y, z, h = SPAWN_POS.x, SPAWN_POS.y, SPAWN_POS.z, SPAWN_HDG
end
exports.spawnmanager:spawnPlayer({
x = x, y = y, z = z, heading = h,
model = data.model or 'mp_m_freemode_01',
skipFade = true
}, function()
-- spawnPlayer resets the model, so the face has to go back on afterwards.
applyAppearance(data.appearance)
local ped = PlayerPedId()
FreezeEntityPosition(ped, false)
SetEntityInvincible(ped, false)
SetEntityVisible(ped, true, false)
SetEntityCollision(ped, true, true)
EnableAllControlActions(0)
SetPlayerInvincible(PlayerId(), false)
DoScreenFadeIn(800)
TriggerEvent('chat:addMessage', {
color = { 0, 113, 227 },
multiline = true,
args = { 'Los Santos', ('Welcome back, %s %s.'):format(data.firstName, data.lastName) }
})
end)
end
RegisterNetEvent('fiverp-chars:spawn', function(data)
if not data.ok then
return SendNUIMessage({ action = 'saveResult', payload = { ok = false, error = data.error } })
end
enterCity(data)
end)
-- Persist where the player stood, so the next session resumes there.
CreateThread(function()
while true do
Wait(60000)
if activeChar then
local c = GetEntityCoords(PlayerPedId())
TriggerServerEvent('fiverp-chars:savePosition', {
id = activeChar, x = c.x, y = c.y, z = c.z, heading = GetEntityHeading(PlayerPedId())
})
end
end
end)
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
if isOpen then SetNuiFocus(false, false) end
stopCamera()
end)
-- --------------------------------------------------------------- callbacks
RegisterNUICallback('startEditor', function(data, cb)
inEditor = true
editSlot = data.slot
editId = data.id
zoom = 'head'
local ped = PlayerPedId()
SetEntityVisible(ped, true, false)
stagePed()
applyAppearance(data.appearance)
stagePed()
sendLimits()
updateCamera()
cb({ ok = true })
end)
RegisterNUICallback('preview', function(data, cb)
if inEditor then
local before = GetEntityModel(PlayerPedId())
applyAppearance(data)
if GetEntityModel(PlayerPedId()) ~= before then
-- The model swap hands back a fresh ped: re-stage it and re-measure the
-- wardrobe, whose drawable counts differ between the two bodies.
stagePed()
applyAppearance(data)
sendLimits()
end
updateCamera()
end
cb({ ok = true })
end)
RegisterNUICallback('rotate', function(data, cb)
local ped = PlayerPedId()
SetEntityHeading(ped, (GetEntityHeading(ped) + (tonumber(data.by) or 0.0)) % 360.0)
updateCamera()
cb({ ok = true })
end)
RegisterNUICallback('zoom', function(data, cb)
zoom = data.part or 'head'
updateCamera()
cb({ ok = true })
end)
RegisterNUICallback('createChar', function(data, cb)
TriggerServerEvent('fiverp-chars:create', data)
cb({ ok = true })
end)
RegisterNUICallback('finishChar', function(data, cb)
TriggerServerEvent('fiverp-chars:finish', data)
cb({ ok = true })
end)
RegisterNUICallback('deleteChar', function(data, cb)
TriggerServerEvent('fiverp-chars:delete', data)
cb({ ok = true })
end)
RegisterNUICallback('playChar', function(data, cb)
TriggerServerEvent('fiverp-chars:play', data)
cb({ ok = true })
end)
RegisterNUICallback('backToSelect', function(data, cb)
inEditor = false
editSlot, editId = nil, nil
zoom = 'body'
local ped = PlayerPedId()
SetEntityVisible(ped, false, false)
stagePed()
SetEntityVisible(ped, false, false)
updateCamera()
TriggerServerEvent('fiverp-chars:request')
cb({ ok = true })
end)