-- 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)