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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
71856b15e9
commit
c857979a55
@@ -0,0 +1,370 @@
|
||||
-- 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)
|
||||
Reference in New Issue
Block a user