Files

3817 lines
104 KiB
Lua

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 <amount>',
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)