print('[how_progression] server.lua loaded') local vinnyFenceSchemaReady = false local function isVinnyFenceUnlocked(value) return value == true or tonumber(value) == 1 end MySQL.ready(function() local ok, err = pcall(function() MySQL.query.await([[ ALTER TABLE how_progression ADD COLUMN IF NOT EXISTS vinny_fence_unlocked TINYINT(1) NOT NULL DEFAULT 0 ]]) MySQL.query.await([[ ALTER TABLE how_progression ADD COLUMN IF NOT EXISTS vinny_fence_day INT NOT NULL DEFAULT 0 ]]) MySQL.query.await([[ ALTER TABLE how_progression ADD COLUMN IF NOT EXISTS vinny_fence_count TINYINT UNSIGNED NOT NULL DEFAULT 0 ]]) end) if not ok then print(('[how_progression] Vinny fence schema migration failed: %s') :format(tostring(err))) return end vinnyFenceSchemaReady = true end) -- ============================================================ -- Progression -- ============================================================ local function getProgression(citizenid) if not citizenid then return nil end local row = MySQL.single.await( 'SELECT * FROM how_progression WHERE citizenid = ?', { citizenid } ) if row then return row end MySQL.insert.await( 'INSERT INTO how_progression (citizenid) VALUES (?)', { citizenid } ) return MySQL.single.await( 'SELECT * FROM how_progression WHERE citizenid = ?', { citizenid } ) end local function getPlayerProgression(src) local player = exports.qbx_core:GetPlayer(src) if not player then return nil, nil end return getProgression(player.PlayerData.citizenid), player end local function setCrowbarProgression(citizenid, stage, rep) if not citizenid then return false end local result = MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = GREATEST(crowbar_stage, ?), crowbar_rep = GREATEST(crowbar_rep, ?) WHERE citizenid = ? ]], { stage, rep, citizenid } ) return result ~= nil end local function advanceCrowbar(src, stageIncrease, repIncrease) local progression, player = getPlayerProgression(src) if not progression or not player then return nil end local currentStage = progression.crowbar_stage or 0 local currentRep = progression.crowbar_rep or 0 local newStage = currentStage + (stageIncrease or 0) local newRep = math.min( currentRep + (repIncrease or 0), 100 ) MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = ?, crowbar_rep = ? WHERE citizenid = ? ]], { newStage, newRep, player.PlayerData.citizenid } ) return newStage, newRep end -- ============================================================ -- Crew / party helpers -- ============================================================ -- Returns a snapshot of the players participating in a mission. -- -- A solo player gets one participant. -- A player in a bs_groupsystem party gets the current party -- members at the moment the mission begins. -- -- Progression belongs to each character individually. -- Mission state belongs to the shared mission. local function getMissionParticipants(src) local player = exports.qbx_core:GetPlayer(src) if not player then return nil, nil end local citizenid = player.PlayerData.citizenid local partyId = exports['bs_groupsystem']:getPlayerPartyId(citizenid) local participants = {} if partyId then exports['bs_groupsystem']:sendToPartyMembers( partyId, function(_, memberCitizenId) if memberCitizenId then -- bs_groupsystem may retain an old FiveM source ID. -- Use the citizenid to resolve the character's current -- live source through Qbox instead. local member = exports.qbx_core:GetPlayerByCitizenId( memberCitizenId ) if member then participants[#participants + 1] = { source = member.PlayerData.source, citizenid = memberCitizenId } end end end ) else participants[1] = { source = src, citizenid = citizenid } end return participants, partyId end local function getCitizenIdFromSource(src) local player = exports.qbx_core:GetPlayer(src) if not player then return nil end return player.PlayerData.citizenid end local function participantIsValid(participant) if not participant then return false end local player = exports.qbx_core:GetPlayer(participant.source) if not player then return false end return player.PlayerData.citizenid == participant.citizenid end local function rewardCrowbarParticipants( participants, requiredStage, nextStage, repReward ) for _, participant in ipairs(participants) do if participantIsValid(participant) then local progression, player = getPlayerProgression(participant.source) if progression and player then local currentStage = progression.crowbar_stage or 0 local currentRep = progression.crowbar_rep or 0 -- Only advance players who were actually at the -- expected mission stage when the mission began. if currentStage == requiredStage then local newRep = math.min( currentRep + repReward, 100 ) MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = ?, crowbar_rep = ? WHERE citizenid = ? ]], { nextStage, newRep, participant.citizenid } ) TriggerClientEvent( 'how_progression:client:crowbarReward', participant.source, repReward, newRep, nextStage ) print(( '[how_progression] Crowbar progression advanced for %s | stage %s | REP %s' ):format( participant.citizenid, nextStage, newRep )) else print(( '[how_progression] %s did not advance: expected stage %s, current stage %s' ):format( participant.citizenid, requiredStage, currentStage )) end end end end end -- ============================================================ -- Mack progression helpers -- ============================================================ local function rewardMackParticipants( participants, requiredStage, nextStage, repReward, cashReward ) for _, participant in ipairs(participants) do if participantIsValid(participant) then local progression, player = getPlayerProgression(participant.source) if progression and player then local currentStage = progression.mack_stage or 0 local currentRep = progression.mack_rep or 0 if currentStage == requiredStage then local newRep = math.min( currentRep + repReward, 100 ) MySQL.update.await( [[ UPDATE how_progression SET mack_stage = ?, mack_rep = ? WHERE citizenid = ? ]], { nextStage, newRep, participant.citizenid } ) if cashReward and cashReward > 0 then player.Functions.AddMoney( 'cash', cashReward, 'mack-mission-reward' ) end TriggerClientEvent( 'how_progression:client:mackReward', participant.source, repReward, newRep, nextStage, cashReward or 0 ) print(( '[how_progression] Mack progression advanced for %s | stage %s | REP %s' ):format( participant.citizenid, nextStage, newRep )) else print(( '[how_progression] %s did not advance Mack: expected stage %s, current stage %s' ):format( participant.citizenid, requiredStage, currentStage )) end end end end end -- ============================================================ -- UNKNOWN introduction -- ============================================================ local function sendSmsToPlayer(src, senderNumber, message) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local citizenid = player.PlayerData.citizenid local phone = player.PlayerData.charinfo.phone if not phone then print(( '[how_progression] SMS failed for %s: no phone number' ):format(citizenid)) return false end local ok, err = pcall(function() exports.npwd:emitMessage({ senderNumber = senderNumber, targetNumber = phone, message = message }) end) if not ok then print(( '[how_progression] SMS failed for %s: %s' ):format(citizenid, tostring(err))) return false end return true end local function introduceUnknown(src) local player = exports.qbx_core:GetPlayer(src) if not player then print(( '[how_progression] Could not find Qbox player for source %s' ):format(tostring(src))) return false end local citizenid = player.PlayerData.citizenid local progression = getProgression(citizenid) if not progression then print(( '[how_progression] Could not create/load progression for %s' ):format(citizenid)) return false end if progression.unknown_introduced == 1 then return false end local phone = player.PlayerData.charinfo.phone if not phone then print(( '[how_progression] No phone number for %s' ):format(citizenid)) return false end local message = "Heard you're new in town. Keep this number. Call me whatever you want. " .. "Online, I go by UNKNOWN.\n\n" .. "If you're looking to make some money, find Crowbar in Sandy. " .. "He's near the Ammu-Nation. He'll have something for you.\n\n" .. "Don't ask him about me. I'll be in touch." local ok, err = pcall(function() exports.npwd:emitMessage({ senderNumber = '555-0100', targetNumber = phone, message = message }) end) if not ok then print(( '[how_progression] UNKNOWN SMS failed for %s: %s' ):format(citizenid, tostring(err))) return false end MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 1, crowbar_stage = GREATEST(crowbar_stage, 1) WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', src ) print(( '[how_progression] UNKNOWN introduced %s to Crowbar' ):format(citizenid)) return true end -- ============================================================ -- Persistent progression state sync -- Restores unlocked world state whenever a character loads. -- ============================================================ local function syncProgressionState(src) local player = exports.qbx_core:GetPlayer(src) if not player then print(('[how_progression] State sync: no player for source %s') :format(tostring(src))) return false end local citizenid = player.PlayerData.citizenid local progression = getProgression(citizenid) if not progression then print(('[how_progression] State sync: no progression for %s') :format(citizenid)) return false end TriggerClientEvent( 'how_progression:client:syncProgression', src, { unknown_introduced = progression.unknown_introduced or 0, crowbar_stage = progression.crowbar_stage or 0, crowbar_rep = progression.crowbar_rep or 0, mack_stage = progression.mack_stage or 0, vinny_fence_unlocked = progression.vinny_fence_unlocked or 0, mack_rep = progression.mack_rep or 0, tina_stage = progression.tina_stage or 0, tina_rep = progression.tina_rep or 0, story_stage = progression.story_stage or 0 } ) print(('[how_progression] Restored persistent state for %s') :format(citizenid)) return true end RegisterNetEvent('QBCore:Server:OnPlayerLoaded') AddEventHandler('QBCore:Server:OnPlayerLoaded', function() local src = source print(('[how_progression] OnPlayerLoaded received from source %s') :format(tostring(src))) CreateThread(function() Wait(3000) local player = exports.qbx_core:GetPlayer(src) if player then print(('[how_progression] Player ready: source=%s citizenid=%s') :format( tostring(src), tostring(player.PlayerData.citizenid) )) else print(('[how_progression] Player NOT ready for source %s') :format(tostring(src))) end syncProgressionState(src) end) end) -- Appearance save is our initial character hook. RegisterNetEvent( 'illenium-appearance:server:saveAppearance', function() local src = source CreateThread(function() Wait(3000) introduceUnknown(src) end) end ) -- ============================================================ -- Development: UNKNOWN -- ============================================================ RegisterCommand('testunknown', function(source) if source == 0 then print('[how_progression] /testunknown must be run by a player') return end local success = introduceUnknown(source) if success then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'UNKNOWN introduction sent.', type = 'success' }) else TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'UNKNOWN introduction was not sent.', type = 'error' }) end end, false) -- ============================================================ -- Crowbar Mission 1 -- The Introduction -- ============================================================ local crowbarMission1 = nil local crowbarMission1ByCitizenId = {} local function clearCrowbarMission1() if not crowbarMission1 then return end if crowbarMission1.vehicle and DoesEntityExist(crowbarMission1.vehicle) then DeleteEntity(crowbarMission1.vehicle) end for _, participant in ipairs(crowbarMission1.participants) do crowbarMission1ByCitizenId[participant.citizenid] = nil end crowbarMission1 = nil end local function startCrowbarMission1(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression( player.PlayerData.citizenid ) if not progression or progression.crowbar_stage ~= 1 then print(( '[how_progression] Mission 1 rejected for %s: invalid stage' ):format(player.PlayerData.citizenid)) return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then print(( '[how_progression] Could not determine Mission 1 participants for %s' ):format(src)) return false end -- Only one Crowbar Mission 1 can be active at a time. clearCrowbarMission1() local vehicle = CreateVehicleServerSetter( `emperor`, 'automobile', 911.40, 3589.50, 33.25, 91.36 ) if not vehicle or vehicle == 0 then print( '[how_progression] Failed to spawn Crowbar Mission 1 vehicle' ) return false end while not DoesEntityExist(vehicle) do Wait(0) end local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end crowbarMission1 = { missionNumber = 1, objective = 'retrieve_vehicle', vehicle = vehicle, netId = NetworkGetNetworkIdFromEntity(vehicle), participants = snapshot, partyId = partyId, completed = false } for _, participant in ipairs(snapshot) do crowbarMission1ByCitizenId[participant.citizenid] = crowbarMission1 exports.qbx_vehiclekeys:GiveKeys( participant.source, vehicle ) TriggerClientEvent( 'how_progression:client:startCrowbarMission1', participant.source, crowbarMission1.netId ) end if partyId then exports['bs_groupsystem']:updatePartyTasks( partyId, { { name = "Steal Crowbar's target vehicle", status = 'current' }, { name = "Bring the vehicle back to Crowbar", status = 'pending' } } ) exports['bs_groupsystem']:sendPartyNotification( partyId, { title = 'Crowbar', description = 'Your crew is on the job.', icon = 'car' } ) end print(( '[how_progression] Crowbar Mission 1 started | owner: %s | participants: %s | party: %s | netId: %s' ):format( player.PlayerData.citizenid, #snapshot, tostring(partyId), tostring(crowbarMission1.netId) )) return true end RegisterNetEvent( 'how_progression:server:startCrowbarMission1', function() startCrowbarMission1(source) end ) local function setCrowbarMission1ReturnPhase(src) local mission = crowbarMission1 if not mission or mission.completed then return false end local citizenid = getCitizenIdFromSource(src) if not citizenid or crowbarMission1ByCitizenId[citizenid] ~= mission then return false end mission.objective = 'return_vehicle' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:crowbarMission1Return', participant.source ) end end if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = "Steal Crowbar's target vehicle", status = 'done' }, { name = "Bring the vehicle back to Crowbar", status = 'current' } } ) end return true end RegisterNetEvent( 'how_progression:server:crowbarMission1VehicleAcquired', function() setCrowbarMission1ReturnPhase(source) end ) local function completeCrowbarMission1(src, netId, devOverride) local mission = crowbarMission1 if not mission or mission.completed then print( '[how_progression] Mission 1 completion rejected: no active mission' ) return false end local citizenid = getCitizenIdFromSource(src) if not citizenid or crowbarMission1ByCitizenId[citizenid] ~= mission then print(( '[how_progression] Mission 1 completion rejected for source %s: not a participant' ):format(src)) return false end if not devOverride then local submittedVehicle = NetworkGetEntityFromNetworkId(netId) if submittedVehicle ~= mission.vehicle then print(( '[how_progression] Mission 1 completion rejected for source %s: vehicle mismatch' ):format(src)) return false end if not DoesEntityExist(mission.vehicle) then print( '[how_progression] Mission 1 completion rejected: vehicle no longer exists' ) return false end if mission.objective ~= 'return_vehicle' then print( '[how_progression] Mission 1 completion rejected: vehicle not acquired' ) return false end end mission.completed = true mission.objective = 'complete' if DoesEntityExist(mission.vehicle) then DeleteEntity(mission.vehicle) end local owner = exports.qbx_core:GetPlayer(src) if not owner then mission.completed = false return false end local cashReward = 500 local repReward = 5 owner.Functions.AddMoney( 'cash', cashReward, 'crowbar-mission-1' ) rewardCrowbarParticipants( mission.participants, 1, 2, repReward ) TriggerClientEvent( 'how_progression:client:crowbarMission1Reward', src, cashReward, repReward ) if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = "Steal Crowbar's target vehicle", status = 'done' }, { name = "Bring the vehicle back to Crowbar", status = 'done' } } ) exports['bs_groupsystem']:sendPartyNotification( mission.partyId, { title = 'Crowbar', description = 'Job done. You proved you can follow directions.', icon = 'car' } ) end print(( '[how_progression] Crowbar Mission 1 complete | owner: %s | $%s | participants: %s' ):format( owner.PlayerData.citizenid, cashReward, #mission.participants )) for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:cleanupCrowbarMission1', participant.source ) end crowbarMission1ByCitizenId[participant.citizenid] = nil end crowbarMission1 = nil return true end RegisterNetEvent( 'how_progression:server:completeCrowbarMission1', function(netId) completeCrowbarMission1( source, netId, false ) end ) -- ============================================================ -- Development cleanup for Mission 1 -- ============================================================ CreateThread(function() Wait(2000) local locations = { vec3(911.40, 3589.50, 33.25), vec3(1705.50, 3771.76, 34.49) } local removed = 0 for _, vehicle in ipairs(GetAllVehicles()) do if DoesEntityExist(vehicle) and GetEntityModel(vehicle) == `emperor` then local coords = GetEntityCoords(vehicle) for _, location in ipairs(locations) do if #(coords - location) < 50.0 then DeleteEntity(vehicle) removed = removed + 1 break end end end end print(( '[how_progression] Cleaned up %s leftover Crowbar Mission 1 Emperor(s)' ):format(removed)) end) -- ============================================================ -- Crowbar Mission 2 -- Get Some Wheels -- ============================================================ local crowbarMission2 = nil local crowbarMission2ByCitizenId = {} local function clearCrowbarMission2() if not crowbarMission2 then return end for _, participant in ipairs(crowbarMission2.participants) do crowbarMission2ByCitizenId[participant.citizenid] = nil end if crowbarMission2.vehicle and DoesEntityExist(crowbarMission2.vehicle) then DeleteEntity(crowbarMission2.vehicle) end crowbarMission2 = nil end local function startCrowbarMission2(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression( player.PlayerData.citizenid ) if not progression or progression.crowbar_stage ~= 2 then print(( '[how_progression] Mission 2 rejected for %s: invalid stage' ):format(player.PlayerData.citizenid)) return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then print(( '[how_progression] Could not determine Mission 2 participants for %s' ):format(src)) return false end clearCrowbarMission2() local vehicle = CreateVehicleServerSetter( `premier`, 'automobile', 2684.30, 3292.18, 54.75, 240.63 ) if not vehicle or vehicle == 0 then print( '[how_progression] Failed to spawn Crowbar Mission 2 vehicle' ) return false end while not DoesEntityExist(vehicle) do Wait(0) end SetVehicleDoorsLocked(vehicle, 2) local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end crowbarMission2 = { missionNumber = 2, objective = 'steal_vehicle', vehicle = vehicle, netId = NetworkGetNetworkIdFromEntity(vehicle), participants = snapshot, partyId = partyId, ownerSource = src, completed = false } for _, participant in ipairs(snapshot) do crowbarMission2ByCitizenId[participant.citizenid] = crowbarMission2 exports.ox_inventory:AddItem( participant.source, 'lockpick', 3 ) TriggerClientEvent( 'how_progression:client:startCrowbarMission2', participant.source, crowbarMission2.netId ) end if partyId then exports['bs_groupsystem']:updatePartyTasks( partyId, { { name = "Find the car Crowbar told you about", status = 'current' }, { name = 'Break into the car', status = 'pending' }, { name = 'Steal the car', status = 'pending' } } ) exports['bs_groupsystem']:sendPartyNotification( partyId, { title = 'Crowbar', description = 'Three lockpicks each. Go get yourselves some wheels.', icon = 'key' } ) end print(( '[how_progression] Crowbar Mission 2 started | owner: %s | participants: %s | party: %s | netId: %s' ):format( player.PlayerData.citizenid, #snapshot, tostring(partyId), tostring(crowbarMission2.netId) )) return true end RegisterNetEvent( 'how_progression:server:startCrowbarMission2', function() startCrowbarMission2(source) end ) local function completeCrowbarMission2(src, netId, props, devOverride) local mission = crowbarMission2 if not mission or mission.completed then print( '[how_progression] Mission 2 completion rejected: no active mission' ) return false end local citizenid = getCitizenIdFromSource(src) if not citizenid or crowbarMission2ByCitizenId[citizenid] ~= mission then print(( '[how_progression] Mission 2 completion rejected for source %s: not a participant' ):format(src)) return false end -- Normal gameplay validates the submitted vehicle and its properties. -- Development completion uses the authoritative server mission vehicle -- and creates a stock owned Premier without requiring client streaming. if not devOverride then local submittedVehicle = NetworkGetEntityFromNetworkId(netId) if submittedVehicle ~= mission.vehicle then print(( '[how_progression] Mission 2 completion rejected for source %s: vehicle mismatch' ):format(src)) return false end if not DoesEntityExist(mission.vehicle) then print( '[how_progression] Mission 2 completion rejected: vehicle no longer exists' ) return false end if GetEntityModel(mission.vehicle) ~= `premier` then print( '[how_progression] Mission 2 completion rejected: wrong vehicle model' ) return false end if type(props) ~= 'table' then print( '[how_progression] Mission 2 completion rejected: missing vehicle properties' ) return false end end mission.completed = true mission.objective = 'complete' local owner = exports.qbx_core:GetPlayerByCitizenId( mission.participants[1].citizenid ) -- Prefer the original mission initiator when still connected. local originalOwner = exports.qbx_core:GetPlayer(mission.ownerSource) if originalOwner then owner = originalOwner end if not owner then mission.completed = false print( '[how_progression] Mission 2 completion rejected: owner unavailable' ) return false end local ownerCitizenId = owner.PlayerData.citizenid local request = { model = 'premier', citizenid = ownerCitizenId } if not devOverride and type(props) == 'table' then request.props = props end local vehicleId, err = exports.qbx_vehicles:CreatePlayerVehicle(request) if not vehicleId then print(( '[how_progression] Failed to create owned Premier for %s: %s' ):format( ownerCitizenId, json.encode(err) )) mission.completed = false mission.objective = 'steal_vehicle' return false end rewardCrowbarParticipants( mission.participants, 2, 3, 5 ) if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = "Find the car Crowbar told you about", status = 'done' }, { name = 'Break into the car', status = 'done' }, { name = 'Steal the car', status = 'done' } } ) exports['bs_groupsystem']:sendPartyNotification( mission.partyId, { title = 'Crowbar', description = 'Job done. The Premier is yours.', icon = 'car' } ) end for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-7384', "Hey, it's Crowbar. Save my contact. " .. "And keep the car—you earned it. " .. "I'll be in touch when I've got more work." ) TriggerClientEvent( 'how_progression:client:cleanupCrowbarMission2', participant.source ) end crowbarMission2ByCitizenId[participant.citizenid] = nil end print(( '[how_progression] Crowbar Mission 2 complete | owner: %s | vehicleId: %s | participants: %s' ):format( ownerCitizenId, tostring(vehicleId), #mission.participants )) -- Do not delete the physical mission Premier. crowbarMission2 = nil return true end RegisterNetEvent( 'how_progression:server:completeCrowbarMission2', function(netId, props) completeCrowbarMission2( source, netId, props, false ) end ) -- ============================================================ -- Crowbar Mission 3: Quick Score -- ============================================================ local crowbarMission3 = nil local crowbarMission3ByCitizenId = {} local function clearCrowbarMission3() if not crowbarMission3 then return end for _, participant in ipairs(crowbarMission3.participants) do crowbarMission3ByCitizenId[participant.citizenid] = nil end crowbarMission3 = nil end local function startCrowbarMission3(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression( player.PlayerData.citizenid ) if not progression or progression.crowbar_stage ~= 3 then print(( '[how_progression] Mission 3 rejected for %s: invalid stage' ):format(player.PlayerData.citizenid)) return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then print(( '[how_progression] Could not determine Mission 3 participants for %s' ):format(src)) return false end clearCrowbarMission3() local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end crowbarMission3 = { missionNumber = 3, objective = 'rob_register', participants = snapshot, partyId = partyId, ownerSource = src, targetStoreIndex = 13, registerRobbed = false, completed = false } for _, participant in ipairs(snapshot) do crowbarMission3ByCitizenId[participant.citizenid] = crowbarMission3 print(( '[how_progression] Mission 3 client target: initiator=%s participant=%s citizenid=%s' ):format( tostring(src), tostring(participant.source), tostring(participant.citizenid) )) TriggerClientEvent( 'how_progression:client:startCrowbarMission3', participant.source ) end -- One unloaded pistol for the crew. exports.ox_inventory:AddItem( src, 'weapon_pistol', 1 ) if partyId then exports['bs_groupsystem']:updatePartyTasks( partyId, { { name = 'Rob the Harmony 24/7 register', status = false }, { name = 'Lose the cops', status = false } } ) exports['bs_groupsystem']:sendPartyNotification( partyId, 'Crowbar: Quick Score' ) end print(( '[how_progression] Crowbar Mission 3 started by %s with %s participant(s)' ):format( player.PlayerData.citizenid, #snapshot )) return true end RegisterNetEvent( 'how_progression:server:startCrowbarMission3', function() startCrowbarMission3(source) end ) AddEventHandler( 'how_progression:server:storeRegisterRobbed', function(src, storeIndex) local mission = crowbarMission3 if not mission or mission.completed then return end if mission.registerRobbed then return end if storeIndex ~= mission.targetStoreIndex then return end local citizenid = getCitizenIdFromSource(src) print(( '[how_progression] M3 robbery validation | src=%s citizenid=%s store=%s member=%s' ):format( tostring(src), tostring(citizenid), tostring(storeIndex), tostring(crowbarMission3ByCitizenId[citizenid] == mission) )) if crowbarMission3ByCitizenId[citizenid] ~= mission then return end mission.registerRobbed = true mission.objective = 'lose_cops' if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Rob the Harmony 24/7 register', status = true }, { name = 'Lose the cops', status = false } } ) end for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:crowbarMission3Escape', participant.source ) end end print(( '[how_progression] Crowbar Mission 3 register robbed | store %s' ):format(storeIndex)) end ) local function completeCrowbarMission3(src, devOverride) local mission = crowbarMission3 if not mission or mission.completed then return false end local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end if crowbarMission3ByCitizenId[citizenid] ~= mission then return false end -- Normal gameplay requires the register robbery first. -- /prog X.2 may bypass the objective for development testing. if not devOverride and not mission.registerRobbed then return false end mission.completed = true mission.objective = 'complete' if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Rob the Harmony 24/7 register', status = true }, { name = 'Lose the cops', status = true } } ) exports['bs_groupsystem']:sendPartyNotification( mission.partyId, 'Quick Score complete' ) end rewardCrowbarParticipants( mission.participants, 3, 4, 5 ) for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-7384', "Not bad. One problem—the cash is marked. " .. "Don't spend it. Come see me. " .. "I know a guy who can clean it." ) end crowbarMission3ByCitizenId[participant.citizenid] = nil end crowbarMission3 = nil print( '[how_progression] Crowbar Mission 3 complete | stage 4' ) return true end RegisterNetEvent( 'how_progression:server:completeCrowbarMission3', function() completeCrowbarMission3(source, false) end ) -- ============================================================ -- Crowbar Mission 4: Wash the Cash -- ============================================================ local crowbarMission4 = nil local crowbarMission4ByCitizenId = {} local function clearCrowbarMission4() if not crowbarMission4 then return end for _, participant in ipairs(crowbarMission4.participants) do crowbarMission4ByCitizenId[participant.citizenid] = nil end crowbarMission4 = nil end local function startCrowbarMission4(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression( player.PlayerData.citizenid ) if not progression or progression.crowbar_stage ~= 4 then return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then return false end clearCrowbarMission4() local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end crowbarMission4 = { missionNumber = 4, objective = 'meet_mack', participants = snapshot, partyId = partyId, ownerSource = src, completed = false } for _, participant in ipairs(snapshot) do crowbarMission4ByCitizenId[participant.citizenid] = crowbarMission4 if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:startCrowbarMission4', participant.source ) end end if partyId then exports['bs_groupsystem']:updatePartyTasks( partyId, { { name = 'Meet Mack at the scrapyard', status = false }, { name = 'Take the marked cash to Gabe', status = false }, { name = 'Wash the marked cash', status = false } } ) exports['bs_groupsystem']:sendPartyNotification( partyId, 'Crowbar: Wash the Cash' ) end return true end RegisterNetEvent( 'how_progression:server:startCrowbarMission4', function() startCrowbarMission4(source) end ) lib.callback.register( 'how_progression:server:canTalkToMack', function(source) local progression, player = getPlayerProgression(source) if not progression or not player then return false end local citizenid = player.PlayerData.citizenid return (progression.mack_stage or 0) >= 1 or crowbarMission4ByCitizenId[citizenid] ~= nil end ) lib.callback.register( 'how_progression:server:getCrowbarMission4State', function(source) local progression, player = getPlayerProgression(source) if not progression or not player then return nil end local mission = crowbarMission4ByCitizenId[ player.PlayerData.citizenid ] if mission and not mission.completed then return mission.objective end if (progression.mack_stage or 0) >= 1 then return 'unlocked' end return nil end ) local function crowbarMission4TalkedToMack(src) local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end local mission = crowbarMission4ByCitizenId[citizenid] if not mission or mission.completed or mission.objective ~= 'meet_mack' then return false end mission.objective = 'meet_gabe' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then MySQL.update.await( [[ UPDATE how_progression SET mack_stage = GREATEST(mack_stage, 1) WHERE citizenid = ? ]], { participant.citizenid } ) TriggerClientEvent( 'how_progression:client:crowbarMission4Mack', participant.source ) end end if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Meet Mack at the scrapyard', status = true }, { name = 'Take the marked cash to Gabe', status = false }, { name = 'Wash the marked cash', status = false } } ) end return true end RegisterNetEvent( 'how_progression:server:crowbarMission4TalkedToMack', function() crowbarMission4TalkedToMack(source) end ) local function crowbarMission4TalkedToGabe(src) local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end local mission = crowbarMission4ByCitizenId[citizenid] if not mission or mission.completed or mission.objective ~= 'meet_gabe' then return false end mission.objective = 'wash_cash' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:crowbarMission4Gabe', participant.source ) end end if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Meet Mack at the scrapyard', status = true }, { name = 'Take the marked cash to Gabe', status = true }, { name = 'Wash the marked cash', status = false } } ) end return true end AddEventHandler( 'how_progression:server:moneywashStarted', function(src) crowbarMission4TalkedToGabe(src) end ) local function completeCrowbarMission4(src, devOverride) local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end local mission = crowbarMission4ByCitizenId[citizenid] if not mission or mission.completed then return false end if not devOverride and mission.objective ~= 'wash_cash' then return false end mission.completed = true mission.objective = 'complete' if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Meet Mack at the scrapyard', status = true }, { name = 'Take the marked cash to Gabe', status = true }, { name = 'Wash the marked cash', status = true } } ) exports['bs_groupsystem']:sendPartyNotification( mission.partyId, 'Wash the Cash complete' ) end rewardCrowbarParticipants( mission.participants, 4, 5, 5 ) for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-7384', "Good. Clean money spends clean. " .. "Come back and see me. " .. "I got something new for ya." ) TriggerClientEvent( 'how_progression:client:cleanupCrowbarMission4', participant.source ) end crowbarMission4ByCitizenId[participant.citizenid] = nil end crowbarMission4 = nil print( '[how_progression] Crowbar Mission 4 complete | stage 5' ) return true end AddEventHandler( 'how_progression:server:moneyWashed', function(src) completeCrowbarMission4(src, false) end ) -- ============================================================ -- Mack Mission 1: Easy Rider -- ============================================================ local mackMission1 = nil local mackMission1ByCitizenId = {} local function clearMackMission1() if not mackMission1 then return end for _, participant in ipairs(mackMission1.participants) do mackMission1ByCitizenId[participant.citizenid] = nil end mackMission1 = nil end local function getMackMission1ForSource(src) local citizenid = getCitizenIdFromSource(src) if not citizenid then return nil end return mackMission1ByCitizenId[citizenid] end local function broadcastMackMission1(mission, eventName, ...) for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( eventName, participant.source, ... ) end end end local function startMackMission1(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression(player.PlayerData.citizenid) if not progression or (progression.mack_stage or 0) ~= 1 then return false end if mackMission1 then TriggerClientEvent( 'ox_lib:notify', src, { title = 'Mack', description = 'I already have somebody handling that delivery.', type = 'error' } ) return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then return false end local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end mackMission1 = { missionNumber = 1, objective = 'leave_scrapyard', participants = snapshot, partyId = partyId, ownerSource = src, bikeCount = math.min(#snapshot, 5), bikesDelivered = {}, deliveredCount = 0, completed = false } for _, participant in ipairs(snapshot) do mackMission1ByCitizenId[ participant.citizenid ] = mackMission1 end for _, participant in ipairs(mackMission1.participants) do if participantIsValid(participant) then local added = exports.ox_inventory:AddItem( participant.source, 'WEAPON_BAT', 1 ) if not added then TriggerClientEvent('ox_lib:notify', participant.source, { title = 'Mack', description = 'Could not carry the bat. Make room in your inventory.', type = 'error' }) end TriggerClientEvent( 'how_progression:client:startMackMission1', participant.source, participant.source == mackMission1.ownerSource, mackMission1.bikeCount ) end end return true end RegisterNetEvent( 'how_progression:server:startMackMission1', function() startMackMission1(source) end ) RegisterNetEvent( 'how_progression:server:mackMission1BikeSpawned', function(bikeIndex, netId) local src = source local mission = getMackMission1ForSource(src) if not mission or mission.completed or src ~= mission.ownerSource then return end bikeIndex = tonumber(bikeIndex) netId = tonumber(netId) if not bikeIndex or bikeIndex < 1 or bikeIndex > mission.bikeCount or not netId then return end CreateThread(function() local vehicle = 0 -- Network ownership can reach the server slightly after -- the spawning client sends us the network ID. for attempt = 1, 20 do vehicle = NetworkGetEntityFromNetworkId(netId) if vehicle and vehicle ~= 0 and DoesEntityExist(vehicle) then break end Wait(100) end if not vehicle or vehicle == 0 or not DoesEntityExist(vehicle) then print(( '[how_progression] M1 failed to resolve bike %d netId %d' ):format(bikeIndex, netId)) return end for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then exports.qbx_vehiclekeys:GiveKeys( participant.source, vehicle, true ) end end end) end ) RegisterNetEvent( 'how_progression:server:mackMission1Detour', function() local mission = getMackMission1ForSource(source) if not mission or mission.completed or mission.objective ~= 'leave_scrapyard' then return end mission.objective = 'trash_car' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-6842', "Hold up. Got something else for you while you're out. Sending you an address." ) TriggerClientEvent( 'how_progression:client:mackMission1TrashCar', participant.source, participant.source == mission.ownerSource ) end end end ) RegisterNetEvent( 'how_progression:server:mackMission1AtDetour', function() local mission = getMackMission1ForSource(source) if not mission or mission.completed or mission.objective ~= 'trash_car' then return end if mission.detourExplained then return end mission.detourExplained = true for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-6842', "See that car? Guy owes me money. I'm tired of asking. Don't kill anybody. Just make sure his next ride is on a tow truck." ) TriggerClientEvent( 'how_progression:client:mackMission1DetourExplained', participant.source ) end end end ) RegisterNetEvent( 'how_progression:server:mackMission1CarTrashed', function() local mission = getMackMission1ForSource(source) if not mission or mission.completed or mission.objective ~= 'trash_car' or not mission.detourExplained or source ~= mission.ownerSource then return end mission.objective = 'deliver_bikes' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-6842', "Yeah. That'll get the point across. Get those bikes to Vinny." ) TriggerClientEvent( 'how_progression:client:mackMission1DeliverBikes', participant.source, mission.deliveredCount, mission.bikeCount ) end end end ) RegisterNetEvent( 'how_progression:server:mackMission1BikeDelivered', function(bikeIndex) local mission = getMackMission1ForSource(source) if not mission or mission.completed or mission.objective ~= 'deliver_bikes' or source ~= mission.ownerSource then return end bikeIndex = tonumber(bikeIndex) if not bikeIndex or bikeIndex < 1 or bikeIndex > mission.bikeCount or mission.bikesDelivered[bikeIndex] then return end mission.bikesDelivered[bikeIndex] = true mission.deliveredCount = mission.deliveredCount + 1 broadcastMackMission1( mission, 'how_progression:client:mackMission1DeliveryCount', mission.deliveredCount, mission.bikeCount ) if mission.deliveredCount >= mission.bikeCount then mission.objective = 'talk_vinny' broadcastMackMission1( mission, 'how_progression:client:mackMission1TalkVinny' ) end end ) RegisterNetEvent( 'how_progression:server:completeMackMission1', function() local mission = getMackMission1ForSource(source) if not mission or mission.completed or mission.objective ~= 'talk_vinny' then return end mission.completed = true mission.objective = 'complete' -- First Mack mission: -- $300 clean cash + 5 Mack REP for every snapshotted participant. rewardMackParticipants( mission.participants, 1, 2, 5, 300 ) broadcastMackMission1( mission, 'how_progression:client:mackMission1Complete' ) -- Vinny follows up after the crew has left. local participants = mission.participants SetTimeout(math.random(60000, 90000), function() for _, participant in ipairs(participants) do if participantIsValid(participant) then local smsSent = sendSmsToPlayer( participant.source, '555-3176', "Hey. Vinny. Told ya you'd like this.\n\n" .. "I buy Sanchezes. Don't care where you find 'em.\n\n" .. "Bring 'em around back. I handle the VIN and paperwork, you get paid.\n\n" .. "Marked bills. Don't ask.\n\n" .. "I can only move so many a day, so don't get greedy." ) if smsSent then getProgression(participant.citizenid) local saved = MySQL.update.await( [[ UPDATE how_progression SET vinny_fence_unlocked = 1 WHERE citizenid = ? ]], { participant.citizenid } ) local progression = saved ~= nil and getProgression(participant.citizenid) if progression and isVinnyFenceUnlocked( progression.vinny_fence_unlocked ) then TriggerClientEvent( 'how_progression:client:unlockVinnyFence', participant.source ) end end end end end) for _, participant in ipairs(participants) do mackMission1ByCitizenId[ participant.citizenid ] = nil end mackMission1 = nil print( '[how_progression] Mack Mission 1 complete | stage 2' ) end ) local vinnyFenceBusy = {} local vinnyFenceCoords = vec3(1356.79, 3617.37, 34.88) lib.callback.register('how_progression:server:fenceSanchez', function(source, netId, gameDay) if not vinnyFenceSchemaReady then return false, 'Vinny is not ready yet. Try again shortly.' end local player = exports.qbx_core:GetPlayer(source) if not player then return false, 'Player unavailable.' end local citizenid = player.PlayerData.citizenid if vinnyFenceBusy[citizenid] then return false, 'Vinny is handling your last bike.' end vinnyFenceBusy[citizenid] = true local function finish(success, message) vinnyFenceBusy[citizenid] = nil return success, message end local progression = getProgression(citizenid) if not progression or not isVinnyFenceUnlocked( progression.vinny_fence_unlocked ) then return finish(false, 'Vinny has not offered to buy bikes yet.') end gameDay = tonumber(gameDay) if not gameDay or gameDay ~= math.floor(gameDay) then return finish(false, 'Could not verify the in-game day.') end local year = math.floor(gameDay / 10000) local month = math.floor(gameDay / 100) % 100 local day = gameDay % 100 if year < 1900 or year > 2100 or month < 0 or month > 12 or day < 1 or day > 31 or gameDay < (progression.vinny_fence_day or 0) then return finish(false, 'Could not verify the in-game day.') end local vehicle = NetworkGetEntityFromNetworkId(tonumber(netId) or 0) local ped = GetPlayerPed(source) if not vehicle or vehicle == 0 or not DoesEntityExist(vehicle) or GetEntityModel(vehicle) ~= `sanchez` or not ped or ped == 0 or GetVehiclePedIsIn(ped, false) ~= vehicle or GetPedInVehicleSeat(vehicle, -1) ~= ped or #(GetEntityCoords(vehicle) - vinnyFenceCoords) > 3.0 or #(GetEntityCoords(ped) - vinnyFenceCoords) > 4.0 or Entity(vehicle).state.howMackMission1Bike then return finish(false, 'Bring an unowned Sanchez into Vinny\'s marker.') end local plate = GetVehicleNumberPlateText(vehicle) if Entity(vehicle).state.vehicleid or exports.qbx_vehicles:GetVehicleIdByPlate(plate) then return finish(false, 'Vinny does not buy player-owned bikes.') end if not exports.ox_inventory:CanCarryItem(source, 'markedbills', 1, { worth = 200 }) then return finish(false, 'Make room for the marked bills first.') end local reserved = MySQL.update.await([[ UPDATE how_progression SET vinny_fence_count = IF(vinny_fence_day = ?, vinny_fence_count + 1, 1), vinny_fence_day = ? WHERE citizenid = ? AND vinny_fence_unlocked = 1 AND (vinny_fence_day < ? OR vinny_fence_count < 3) ]], { gameDay, gameDay, citizenid, gameDay }) if reserved ~= 1 then return finish(false, 'Vinny has bought three Sanchezes from you today.') end local function releaseQuota() MySQL.update.await([[ UPDATE how_progression SET vinny_fence_count = GREATEST(vinny_fence_count - 1, 0) WHERE citizenid = ? AND vinny_fence_day = ? ]], { citizenid, gameDay }) end TriggerClientEvent('how_progression:client:leaveFenceBike', source, netId) local timeout = GetGameTimer() + 4000 while GetVehiclePedIsIn(ped, false) == vehicle and GetGameTimer() < timeout do Wait(100) end if not DoesEntityExist(vehicle) or GetVehiclePedIsIn(ped, false) == vehicle or #(GetEntityCoords(vehicle) - vinnyFenceCoords) > 3.0 or #(GetEntityCoords(ped) - vinnyFenceCoords) > 5.0 then releaseQuota() return finish(false, 'Leave the Sanchez parked in the marker.') end DeleteEntity(vehicle) Wait(100) if DoesEntityExist(vehicle) then releaseQuota() return finish(false, 'Vinny could not take that bike. Try again.') end local added = exports.ox_inventory:AddItem(source, 'markedbills', 1, { worth = 200 }) if not added then releaseQuota() return finish(false, 'The marked bills could not be added to your inventory.') end return finish(true, 'Vinny paid you $200 in marked bills.') end) lib.callback.register( 'how_progression:server:getMackMission1State', function(source) local progression, player = getPlayerProgression(source) if not progression or not player then return nil end local mission = mackMission1ByCitizenId[ player.PlayerData.citizenid ] if mission and not mission.completed then return { objective = mission.objective, deliveredCount = mission.deliveredCount, bikeCount = mission.bikeCount } end if (progression.mack_stage or 0) == 1 then return { objective = 'available', deliveredCount = 0 } end if (progression.mack_stage or 0) >= 2 then return { objective = 'complete', deliveredCount = 3 } end return nil end ) -- ============================================================ -- Crowbar Mission 5: Buy In -- ============================================================ local crowbarMission5 = nil local crowbarMission5ByCitizenId = {} local function clearCrowbarMission5() if not crowbarMission5 then return end for _, participant in ipairs(crowbarMission5.participants) do crowbarMission5ByCitizenId[ participant.citizenid ] = nil end crowbarMission5 = nil end local function startCrowbarMission5(src) local player = exports.qbx_core:GetPlayer(src) if not player then return false end local progression = getProgression( player.PlayerData.citizenid ) if not progression or progression.crowbar_stage ~= 5 then return false end local participants, partyId = getMissionParticipants(src) if not participants or #participants == 0 then return false end clearCrowbarMission5() local snapshot = {} for _, participant in ipairs(participants) do snapshot[#snapshot + 1] = { source = participant.source, citizenid = participant.citizenid } end local stashId = exports.ox_inventory:CreateTemporaryStash({ label = "Crowbar's Package", slots = 1, maxWeight = 5000, items = { { 'crowbar_package', 1, { description = "A sealed package. Crowbar wants it unopened." } } } }) if not stashId then return false end crowbarMission5 = { missionNumber = 5, objective = 'get_package', participants = snapshot, partyId = partyId, ownerSource = src, stashId = stashId, completed = false } for _, participant in ipairs(snapshot) do crowbarMission5ByCitizenId[ participant.citizenid ] = crowbarMission5 if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:startCrowbarMission5', participant.source, stashId ) end end return true end RegisterNetEvent( 'how_progression:server:startCrowbarMission5', function() startCrowbarMission5(source) end ) local function crowbarMission5PackageTaken(src) local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end local mission = crowbarMission5ByCitizenId[citizenid] if not mission or mission.completed or mission.objective ~= 'get_package' then return false end mission.objective = 'escape_ambush' mission.carrierSource = src for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then sendSmsToPlayer( participant.source, '555-7384', "Get out of there! You're about to have company! Lose em!" ) end end print(( '[how_progression] M5 package taken | carrier=%s | participants=%s' ):format( tostring(src), tostring(#mission.participants) )) for _, participant in ipairs(mission.participants) do local valid = participantIsValid(participant) print(( '[how_progression] M5 participant | source=%s citizenid=%s valid=%s ambushOwner=%s' ):format( tostring(participant.source), tostring(participant.citizenid), tostring(valid), tostring(participant.source == src) )) if valid then TriggerClientEvent( 'how_progression:client:crowbarMission5PackageTaken', participant.source, participant.source == src ) end end return true end RegisterNetEvent( 'how_progression:server:crowbarMission5Escaped', function() local src = source local citizenid = getCitizenIdFromSource(src) if not citizenid then return end local mission = crowbarMission5ByCitizenId[citizenid] if not mission or mission.completed or mission.objective ~= 'escape_ambush' or mission.carrierSource ~= src then return end mission.objective = 'return_package' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:crowbarMission5Escaped', participant.source ) end end end ) exports.ox_inventory:registerHook( 'swapItems', function(payload) if not crowbarMission5 then return end if crowbarMission5.objective ~= 'get_package' then return end local fromInventory = payload.fromInventory local fromId = fromInventory if type(fromInventory) == 'table' then fromId = fromInventory.id or fromInventory.name end if tostring(fromId) ~= tostring(crowbarMission5.stashId) then return end -- Moving the package from Crowbar's stash into a -- player's inventory starts the ambush. if payload.toType ~= 'player' then return end local itemName if type(payload.fromSlot) == 'table' then itemName = payload.fromSlot.name end if itemName ~= 'crowbar_package' then return end -- swapItems is a pre-hook. Let ox_inventory finish -- moving the item, then advance the mission. SetTimeout(250, function() crowbarMission5PackageTaken( payload.source ) end) end, { itemFilter = { crowbar_package = true } } ) local function completeCrowbarMission5( src, devOverride ) local citizenid = getCitizenIdFromSource(src) if not citizenid then return false end local mission = crowbarMission5ByCitizenId[citizenid] if not mission or mission.completed then return false end if not devOverride and mission.objective ~= 'return_package' then return false end if not devOverride then local count = exports.ox_inventory:GetItemCount( src, 'crowbar_package' ) if not count or count < 1 then TriggerClientEvent( 'ox_lib:notify', src, { title = 'Crowbar', description = "You don't have my package.", type = 'error' } ) return false end exports.ox_inventory:RemoveItem( src, 'crowbar_package', 1 ) end mission.completed = true mission.objective = 'complete' rewardCrowbarParticipants( mission.participants, 5, 6, 5 ) -- The carrier gets the cash payment. local carrier = exports.qbx_core:GetPlayer(src) if carrier then carrier.Functions.AddMoney( 'cash', 750, 'crowbar-buy-in' ) end for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:cleanupCrowbarMission5', participant.source ) TriggerClientEvent( 'how_progression:client:crowbarMission5Complete', participant.source ) end crowbarMission5ByCitizenId[ participant.citizenid ] = nil end crowbarMission5 = nil print( '[how_progression] Crowbar Mission 5 complete | stage 6' ) return true end RegisterNetEvent( 'how_progression:server:completeCrowbarMission5', function() completeCrowbarMission5( source, false ) end ) -- ============================================================ -- Crowbar active mission state -- ============================================================ local function getCrowbarActiveMission(citizenid) local checks = { { mission = crowbarMission1, members = crowbarMission1ByCitizenId }, { mission = crowbarMission2, members = crowbarMission2ByCitizenId }, { mission = crowbarMission3, members = crowbarMission3ByCitizenId }, { mission = crowbarMission4, members = crowbarMission4ByCitizenId }, { mission = crowbarMission5, members = crowbarMission5ByCitizenId } } for _, check in ipairs(checks) do local mission = check.mission if mission and not mission.completed and check.members[citizenid] == mission then return { missionNumber = mission.missionNumber, objective = mission.objective } end end return nil end -- ============================================================ -- Crowbar progression callback -- ============================================================ lib.callback.register( 'how_progression:server:getCrowbarMission', function(source) local progression = getPlayerProgression(source) if not progression then return 0 end return progression.crowbar_stage or 0 end ) lib.callback.register( 'how_progression:server:isCrowbarMissionActive', function(source) local citizenid = getCitizenIdFromSource(source) if not citizenid then return false end return getCrowbarActiveMission(citizenid) ~= nil end ) -- ============================================================ -- Development: skip Crowbar Mission 1 -- ============================================================ RegisterCommand('skipcrowbar1', function(source) if source == 0 then print( '[how_progression] /skipcrowbar1 must be run by a player' ) return end local progression, player = getPlayerProgression(source) if not progression or not player then return end local currentRep = progression.crowbar_rep or 0 local newRep = math.min(currentRep + 5, 100) MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 1, crowbar_stage = GREATEST(crowbar_stage, 2), crowbar_rep = GREATEST(crowbar_rep, ?) WHERE citizenid = ? ]], { newRep, player.PlayerData.citizenid } ) TriggerClientEvent( 'how_progression:client:crowbarReward', source, 5, newRep, 2 ) print(( '[how_progression] %s skipped Crowbar Mission 1 | stage 2 | REP %s' ):format( player.PlayerData.citizenid, newRep )) end, false) -- ============================================================ -- Development: progression control -- Usage: -- /prog crowbar 0 -- /prog crowbar 3 -- /prog mack 3 -- /prog tina 5 -- ============================================================ RegisterCommand('prog', function(source, args) if source == 0 then print('[how_progression] /prog must be run by a player') return end local contact = args[1] and string.lower(args[1]) local stageInput = args[2] -- Complete development reset. if contact == 'reset' then local player = exports.qbx_core:GetPlayer(source) if not player then return end local citizenid = player.PlayerData.citizenid -- Abort all currently active Crowbar missions. clearCrowbarMission1() clearCrowbarMission2() clearCrowbarMission3() clearCrowbarMission4() clearCrowbarMission5() -- Reset all custom progression, including UNKNOWN. MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 0, crowbar_stage = 0, crowbar_rep = 0, mack_stage = 0, vinny_fence_unlocked = 0, vinny_fence_day = 0, vinny_fence_count = 0, mack_rep = 0, tina_stage = 0, tina_rep = 0, story_stage = 0 WHERE citizenid = ? ]], { citizenid } ) -- Development reset intentionally removes everything -- currently carried by this character. -- Reset inventory to the standard development baseline. exports.ox_inventory:ClearInventory(source) exports.ox_inventory:AddItem( source, 'phone', 1 ) exports.ox_inventory:AddItem( source, 'money', 500 ) -- Clear local mission state, temporary routes/blips, -- and locked contact blips. TriggerClientEvent( 'how_progression:client:resetProgression', source ) print(( '[how_progression] Full progression reset for %s' ):format(citizenid)) return end -- UNKNOWN development control. -- -- /prog unknown 0 = reset UNKNOWN introduction state -- /prog unknown 1 = perform the real UNKNOWN introduction if contact == 'unknown' then local player = exports.qbx_core:GetPlayer(source) if not player then return end local citizenid = player.PlayerData.citizenid if stageInput == '0' then MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 0, crowbar_stage = 0 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:resetProgression', source ) TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'UNKNOWN reset.', type = 'success' }) return elseif stageInput == '1' then if not introduceUnknown(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'UNKNOWN introduction was not sent.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'UNKNOWN introduction triggered.', type = 'success' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Use /prog unknown 0 or /prog unknown 1.', type = 'error' }) return end if not contact or not stageInput then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Usage: /prog crowbar 0-10 or /prog crowbar 1.1', type = 'error' }) return end -- Development mission shortcuts: -- X = mission available -- X.Y = mission X, phase Y -- -- Each mission defines what its phases mean. The final -- phase always performs the real mission completion path. local missionInput, phaseInput = stageInput:match('^(%d+)%.(%d+)$') local stage local phase if missionInput then stage = tonumber(missionInput) phase = tonumber(phaseInput) if not phase or phase < 1 then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission phase must be 1 or greater.', type = 'error' }) return end else if not stageInput:match('^%d+$') then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Use X or X.Y, for example 1, 1.1, or 1.2.', type = 'error' }) return end stage = tonumber(stageInput) end if not stage or stage < 0 or stage > 10 then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Stage must be between 0 and 10.', type = 'error' }) return end local player = exports.qbx_core:GetPlayer(source) if not player then return end local citizenid = player.PlayerData.citizenid local column if contact == 'crowbar' then column = 'crowbar_stage' elseif contact == 'mack' then column = 'mack_stage' elseif contact == 'tina' then column = 'tina_stage' else TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Valid contacts: crowbar, mack, tina', type = 'error' }) return end -- Mission phase shortcuts. if phase then if contact ~= 'crowbar' then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission phase shortcuts are currently only implemented for Crowbar.', type = 'error' }) return end -- ==================================================== -- Crowbar Mission 1 -- 1.1 = accepted / retrieve Emperor -- 1.2 = Emperor acquired / return to Crowbar -- 1.3 = completion -- ==================================================== if stage == 1 then if phase == 1 then MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = 1, unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) if not startCrowbarMission1(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Could not start Crowbar Mission 1.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 1 started.', type = 'success' }) return elseif phase == 2 then if not setCrowbarMission1ReturnPhase(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 1 is not currently active.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 1 advanced to return phase.', type = 'success' }) return elseif phase == 3 then if not completeCrowbarMission1( source, nil, true ) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 1 could not be completed.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 1 completed.', type = 'success' }) return end -- ==================================================== -- Crowbar Mission 2 -- 2.1 = accepted / steal Premier -- 2.2 = completion -- ==================================================== elseif stage == 2 then if phase == 1 then MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = 2, unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) if not startCrowbarMission2(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Could not start Crowbar Mission 2.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 2 started.', type = 'success' }) return elseif phase == 2 then if not completeCrowbarMission2( source, nil, nil, true ) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 2 could not be completed.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 2 completed.', type = 'success' }) return end -- ==================================================== -- Crowbar Mission 3 -- 3.1 = accepted / rob Harmony register -- 3.2 = register robbed / lose cops -- 3.3 = completion -- ==================================================== elseif stage == 3 then if phase == 1 then MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = 3, unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) if not startCrowbarMission3(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Could not start Crowbar Mission 3.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 3 started.', type = 'success' }) return elseif phase == 2 then local mission = crowbarMission3 if not mission or mission.completed or crowbarMission3ByCitizenId[citizenid] ~= mission then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 3 is not currently active.', type = 'error' }) return end mission.registerRobbed = true mission.objective = 'lose_cops' for _, participant in ipairs(mission.participants) do if participantIsValid(participant) then TriggerClientEvent( 'how_progression:client:crowbarMission3Escape', participant.source ) end end if mission.partyId then exports['bs_groupsystem']:updatePartyTasks( mission.partyId, { { name = 'Rob the Harmony 24/7 register', status = 'done' }, { name = 'Lose the cops', status = 'current' } } ) end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 3 advanced to escape phase.', type = 'success' }) return elseif phase == 3 then if not completeCrowbarMission3(source, true) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 3 could not be completed.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 3 completed.', type = 'success' }) return end end -- ==================================================== -- Crowbar Mission 4: Wash the Cash -- 4.1 = accepted / meet Mack -- 4.2 = Mack spoken to / meet Gabe -- 4.3 = Gabe contacted / wash cash -- 4.4 = completion -- ==================================================== if stage == 4 then if phase == 1 then MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = 4, unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) if not startCrowbarMission4(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Could not start Crowbar Mission 4.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 4 started.', type = 'success' }) return elseif phase == 2 then if not crowbarMission4TalkedToMack(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission 4 is not waiting for Mack.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission 4 advanced to Gabe.', type = 'success' }) return elseif phase == 3 then if not crowbarMission4TalkedToGabe(source) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission 4 is not waiting for Gabe.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Mission 4 advanced to money washing.', type = 'success' }) return elseif phase == 4 then if not completeCrowbarMission4(source, true) then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 4 could not be completed.', type = 'error' }) return end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Crowbar Mission 4 completed.', type = 'success' }) return end end -- Crowbar Mission 5: Buy In if stage == 5 then if phase == 1 then MySQL.update.await( [[ UPDATE how_progression SET crowbar_stage = 5, unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) if not startCrowbarMission5(source) then TriggerClientEvent( 'ox_lib:notify', source, { title = 'Development', description = 'Could not start Crowbar Mission 5.', type = 'error' } ) end return elseif phase == 2 then if not crowbarMission5PackageTaken(source) then TriggerClientEvent( 'ox_lib:notify', source, { title = 'Development', description = 'Mission 5 could not advance to the ambush.', type = 'error' } ) end return elseif phase == 3 then -- Fight or escape: there is deliberately no -- kill requirement. This represents reaching -- the return-package phase. local mission = crowbarMission5ByCitizenId[citizenid] if mission then mission.objective = 'return_package' TriggerClientEvent( 'how_progression:client:crowbarMission5PackageTaken', source ) end return elseif phase == 4 then completeCrowbarMission5( source, true ) return end end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = ('Crowbar Mission %s phase %s is not implemented.'):format( stage, phase ), type = 'error' }) return end -- Normal progression stage. MySQL.update.await( ('UPDATE how_progression SET %s = ? WHERE citizenid = ?'):format(column), { stage, citizenid } ) -- Setting Crowbar to 0 also resets his introduction and REP. if contact == 'crowbar' and stage == 0 then MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 0, crowbar_rep = 0 WHERE citizenid = ? ]], { citizenid } ) end -- Active Crowbar stages require the character to have been introduced. if contact == 'crowbar' and stage > 0 then MySQL.update.await( [[ UPDATE how_progression SET unknown_introduced = 1 WHERE citizenid = ? ]], { citizenid } ) TriggerClientEvent( 'how_progression:client:unlockCrowbar', source ) end TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = ( '%s progression set to stage %s.' ):format( contact:gsub("^%l", string.upper), stage ), type = 'success' }) print(( '[how_progression] %s set %s progression to stage %s' ):format( citizenid, contact, stage )) end, false) -- ============================================================ -- Resource cleanup -- ============================================================ AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end if crowbarMission1 then if crowbarMission1.vehicle and DoesEntityExist(crowbarMission1.vehicle) then DeleteEntity(crowbarMission1.vehicle) end end if crowbarMission2 then if crowbarMission2.vehicle and DoesEntityExist(crowbarMission2.vehicle) then DeleteEntity(crowbarMission2.vehicle) end end end) -- Development helper: give properly formatted marked bills. lib.addCommand('givemarked', { help = 'Give yourself marked bills for development testing', params = { { name = 'amount', type = 'number', help = 'Dollar value of the marked bills' } }, restricted = false }, function(source, args) local amount = math.floor(tonumber(args.amount) or 0) if amount <= 0 then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = 'Usage: /givemarked ', type = 'error' }) return end local success = exports.ox_inventory:AddItem( source, 'markedbills', 1, { worth = amount } ) if success then TriggerClientEvent('ox_lib:notify', source, { title = 'Development', description = ('$%s in marked bills added.'):format(amount), type = 'success' }) end end) RegisterNetEvent( 'how_progression:server:crowbarPackageCarrierDied', function() local src = source local count = exports.ox_inventory:Search( src, 'count', 'crowbar_package' ) or 0 if count > 0 then exports.ox_inventory:RemoveItem( src, 'crowbar_package', count ) TriggerClientEvent( 'ox_lib:notify', src, { title = 'Buy In', description = "Crowbar's package was lost.", type = 'error' } ) end end ) -- Temporary NPWD contact test RegisterCommand('testcontact', function(source) if source == 0 then return end local number = ('555-%04d'):format(math.random(0, 9999)) local contact = exports.npwd:addContact(source, { display = 'Michael', number = number, avatar = 'https://fivemedia.howlerdesign.com/media/6801a2d6-659f-46e3-b0ee-028a9a4ac215.png' }) if not contact then print('[how_progression] Failed to add NPWD test contact') return end TriggerClientEvent('how_progression:client:addPhoneContact', source, contact) SetTimeout(500, function() sendSmsToPlayer(source, number, 'Hey. Testing the new contact system.') end) end, false)