mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 02:01:40 +00:00
64a6e58577
Implement SendNotification compatibility for lb-phone: validate app IDs, normalize and validate title/content, check provider/registered apps and reserved IDs, and dispatch a notification NUI message. Register the export and a compatibility alias. Update client tests to assert successful delivery to bundled apps and proper rejection for unknown app IDs.
683 lines
21 KiB
Lua
683 lines
21 KiB
Lua
local RESOURCE_NAME = GetCurrentResourceName()
|
|
local providers = SkyPhoneCompatibility.Providers
|
|
local core = SkyPhoneApps.CompatibilityCore
|
|
local provider_apps = {}
|
|
local high_client_apps = {}
|
|
local high_server_apps = {}
|
|
|
|
local function get_calling_resource(export_name)
|
|
local owner_resource = GetInvokingResource()
|
|
if owner_resource then
|
|
return owner_resource
|
|
end
|
|
|
|
print(("[%s] %s rejected: the export must be called by another resource."):format(
|
|
RESOURCE_NAME,
|
|
export_name
|
|
))
|
|
return nil, "invalid_owner"
|
|
end
|
|
|
|
local function copy_record_data(value)
|
|
if type(value) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local copied = {}
|
|
for key, nested_value in pairs(value) do
|
|
copied[key] = nested_value
|
|
end
|
|
return copied
|
|
end
|
|
|
|
local function register_provider_app(provider, owner_resource, definition, vendor_data)
|
|
local app_id = definition.id
|
|
local existing = provider_apps[app_id]
|
|
if existing and (existing.owner_resource ~= owner_resource or existing.provider ~= provider) then
|
|
return false, "duplicate_app_id"
|
|
end
|
|
|
|
local success, error_message
|
|
if existing then
|
|
success, error_message = core.Update(owner_resource, definition)
|
|
else
|
|
success, error_message = core.Add(owner_resource, definition)
|
|
end
|
|
if success then
|
|
provider_apps[app_id] = {
|
|
definition = definition,
|
|
owner_resource = owner_resource,
|
|
provider = provider,
|
|
vendor_data = vendor_data,
|
|
}
|
|
end
|
|
return success, error_message
|
|
end
|
|
|
|
local function get_provider_app(owner_resource, app_id, allowed_providers)
|
|
if type(app_id) ~= "string" then
|
|
return nil, "invalid_app_id"
|
|
end
|
|
|
|
local record = provider_apps[app_id]
|
|
if not record then
|
|
return nil, "app_not_found"
|
|
end
|
|
if record.owner_resource ~= owner_resource then
|
|
return nil, "app_owner_mismatch"
|
|
end
|
|
if allowed_providers and not allowed_providers[record.provider] then
|
|
return nil, "app_provider_mismatch"
|
|
end
|
|
return record
|
|
end
|
|
|
|
local function remove_provider_app(owner_resource, app_id, allowed_providers)
|
|
local record, record_error = get_provider_app(owner_resource, app_id, allowed_providers)
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
|
|
return core.Remove(owner_resource, app_id)
|
|
end
|
|
|
|
function SkyPhoneApps.RegisterCompatibilityExport(owner_resource, app_data)
|
|
local provider
|
|
local definition
|
|
local definition_error
|
|
if app_data.identifier ~= nil then
|
|
provider = providers.lb
|
|
definition, definition_error = SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data)
|
|
elseif app_data.key ~= nil then
|
|
provider = providers.yseries
|
|
definition, definition_error = SkyPhoneCompatibility.BuildYSeriesDefinition(app_data)
|
|
else
|
|
return false, "unknown_app_provider"
|
|
end
|
|
|
|
if not definition then
|
|
print(("[%s] %s registration rejected for %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
provider,
|
|
owner_resource,
|
|
definition_error
|
|
))
|
|
return false, definition_error
|
|
end
|
|
return register_provider_app(provider, owner_resource, definition, copy_record_data(app_data))
|
|
end
|
|
|
|
SkyPhoneApps.OnCompatibilityAppRemoved = function(owner_resource, app_id)
|
|
local record = provider_apps[app_id]
|
|
if record and record.owner_resource == owner_resource then
|
|
provider_apps[app_id] = nil
|
|
end
|
|
end
|
|
|
|
local function add_17mov_application(app_data)
|
|
local owner_resource, owner_error = get_calling_resource("AddApplication")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local definition, definition_error = SkyPhoneCompatibility.Build17MovDefinition(app_data)
|
|
if not definition then
|
|
print(("[%s] 17mov registration rejected for %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
owner_resource,
|
|
definition_error
|
|
))
|
|
return false, definition_error
|
|
end
|
|
return register_provider_app(
|
|
providers.seventeen,
|
|
owner_resource,
|
|
definition,
|
|
copy_record_data(app_data)
|
|
)
|
|
end
|
|
|
|
local function remove_17mov_application(app_name, _resource_name)
|
|
local owner_resource, owner_error = get_calling_resource("RemoveApplication")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local app_id = type(app_name) == "table" and app_name.name or app_name
|
|
return remove_provider_app(owner_resource, app_id, {
|
|
[providers.seventeen] = true,
|
|
})
|
|
end
|
|
|
|
local function send_app_message(app_id, data)
|
|
local owner_resource, owner_error = get_calling_resource("SendAppMessage")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local record, record_error = get_provider_app(owner_resource, app_id, {
|
|
[providers.seventeen] = true,
|
|
[providers.yseries] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
return core.SendMessage(owner_resource, app_id, data)
|
|
end
|
|
|
|
local function add_high_application(app_name, data, locales)
|
|
local owner_resource, owner_error = get_calling_resource("addApplication")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local definition, definition_error = SkyPhoneCompatibility.BuildHighDefinition(
|
|
owner_resource,
|
|
app_name,
|
|
data,
|
|
locales
|
|
)
|
|
if not definition then
|
|
print(("[%s] High Phone registration rejected for %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
owner_resource,
|
|
definition_error
|
|
))
|
|
return false, definition_error
|
|
end
|
|
|
|
high_client_apps[app_name] = {
|
|
definition = definition,
|
|
owner_resource = owner_resource,
|
|
}
|
|
local server_record = high_server_apps[app_name]
|
|
if server_record then
|
|
if server_record.owner_resource ~= owner_resource then
|
|
return false, "duplicate_app_id"
|
|
end
|
|
if server_record.registered then
|
|
return true
|
|
end
|
|
return false, "server_definition_not_registered"
|
|
end
|
|
|
|
return register_provider_app(
|
|
providers.high,
|
|
owner_resource,
|
|
definition,
|
|
copy_record_data(data)
|
|
)
|
|
end
|
|
|
|
local function send_high_app_nui(app_name, data)
|
|
local owner_resource, owner_error = get_calling_resource("sendAppNui")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local record, record_error = get_provider_app(owner_resource, app_name, {
|
|
[providers.high] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
return core.SendMessage(owner_resource, app_name, data)
|
|
end
|
|
|
|
local function add_quasar_app_for_owner(owner_resource, app_data)
|
|
local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(app_data)
|
|
if not definition then
|
|
print(("[%s] Quasar registration rejected for %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
owner_resource,
|
|
definition_error
|
|
))
|
|
return false, definition_error
|
|
end
|
|
|
|
return register_provider_app(
|
|
providers.quasar,
|
|
owner_resource,
|
|
definition,
|
|
SkyPhoneCompatibility.CopyQuasarData(app_data)
|
|
)
|
|
end
|
|
|
|
local function add_quasar_app(app_data)
|
|
local owner_resource, owner_error = get_calling_resource("addCustomApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
if type(app_data) ~= "table" then
|
|
return false, "invalid_app_data"
|
|
end
|
|
return add_quasar_app_for_owner(owner_resource, app_data)
|
|
end
|
|
|
|
local function add_quasar_apps_batch(apps)
|
|
local owner_resource, owner_error = get_calling_resource("addCustomAppsBatch")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
if type(apps) ~= "table" then
|
|
return false, "invalid_app_batch"
|
|
end
|
|
|
|
for index = 1, #apps do
|
|
local success, error_message = add_quasar_app_for_owner(owner_resource, apps[index])
|
|
if not success then
|
|
return false, error_message
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function update_quasar_app(app_id, patch)
|
|
local owner_resource, owner_error = get_calling_resource("updateCustomApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
if type(patch) ~= "table" then
|
|
return false, "invalid_patch"
|
|
end
|
|
|
|
local record, record_error = get_provider_app(owner_resource, app_id, {
|
|
[providers.quasar] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
|
|
local merged = SkyPhoneCompatibility.CopyQuasarData(record.vendor_data)
|
|
for key, value in pairs(patch) do
|
|
if key == "iframe" and type(value) == "table" then
|
|
merged.iframe = merged.iframe or {}
|
|
for iframe_key, iframe_value in pairs(value) do
|
|
merged.iframe[iframe_key] = iframe_value
|
|
end
|
|
elseif key ~= "id" then
|
|
merged[key] = value
|
|
end
|
|
end
|
|
merged.id = app_id
|
|
|
|
local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(merged)
|
|
if not definition then
|
|
return false, definition_error
|
|
end
|
|
|
|
local success, error_message = core.Update(owner_resource, definition)
|
|
if success then
|
|
record.definition = definition
|
|
record.vendor_data = SkyPhoneCompatibility.CopyQuasarData(merged)
|
|
end
|
|
return success, error_message
|
|
end
|
|
|
|
local function remove_quasar_app(app_id)
|
|
local owner_resource, owner_error = get_calling_resource("removeCustomApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
return remove_provider_app(owner_resource, app_id, {
|
|
[providers.quasar] = true,
|
|
})
|
|
end
|
|
|
|
local function get_quasar_apps()
|
|
local ids = {}
|
|
for app_id, record in pairs(provider_apps) do
|
|
if record.provider == providers.quasar then
|
|
ids[#ids + 1] = app_id
|
|
end
|
|
end
|
|
table.sort(ids)
|
|
|
|
local apps = {}
|
|
for index = 1, #ids do
|
|
apps[index] = SkyPhoneCompatibility.CopyQuasarData(provider_apps[ids[index]].vendor_data)
|
|
end
|
|
return apps
|
|
end
|
|
|
|
local function open_quasar_app(app_id)
|
|
local owner_resource, owner_error = get_calling_resource("OpenPhoneApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local record, record_error = get_provider_app(owner_resource, app_id, {
|
|
[providers.quasar] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
return core.Open(owner_resource, app_id)
|
|
end
|
|
|
|
local function open_phone_app(app_id, data)
|
|
local owner_resource, owner_error = get_calling_resource("OpenApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local record, record_error = get_provider_app(owner_resource, app_id)
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
return core.Open(owner_resource, app_id, data)
|
|
end
|
|
|
|
local function close_phone_app(options)
|
|
local owner_resource, owner_error = get_calling_resource("CloseApp")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local app_id = type(options) == "table" and options.app or nil
|
|
if app_id then
|
|
local record, record_error = get_provider_app(owner_resource, app_id, {
|
|
[providers.lb] = true,
|
|
[providers.yseries] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
return core.Close(owner_resource, app_id)
|
|
end
|
|
return core.CloseActive(owner_resource)
|
|
end
|
|
|
|
local function normalize_lb_notification_text(value, maximum_length, error_code)
|
|
if type(value) ~= "string" then
|
|
return nil, error_code
|
|
end
|
|
|
|
local normalized = value:match("^%s*(.-)%s*$")
|
|
if normalized == "" or #normalized > maximum_length then
|
|
return nil, error_code
|
|
end
|
|
return normalized
|
|
end
|
|
|
|
local function send_lb_notification(data)
|
|
local owner_resource, owner_error = get_calling_resource("SendNotification")
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
if type(data) ~= "table" then
|
|
return false, "invalid_notification"
|
|
end
|
|
|
|
local app_id = data.app or data.identifier
|
|
local valid_app_id, app_id_error = SkyPhoneApps.ValidateAppId(app_id)
|
|
if not valid_app_id then
|
|
return false, app_id_error
|
|
end
|
|
|
|
local registered_record = provider_apps[app_id]
|
|
if registered_record then
|
|
local record, record_error = get_provider_app(owner_resource, app_id, {
|
|
[providers.lb] = true,
|
|
})
|
|
if not record then
|
|
return false, record_error
|
|
end
|
|
elseif not SkyPhoneApps.ReservedAppIds[app_id] then
|
|
return false, "app_not_found"
|
|
end
|
|
|
|
local title, title_error = normalize_lb_notification_text(
|
|
data.title,
|
|
128,
|
|
"invalid_notification_title"
|
|
)
|
|
if not title then
|
|
return false, title_error
|
|
end
|
|
|
|
local content, content_error = normalize_lb_notification_text(
|
|
data.content or data.message or data.text,
|
|
512,
|
|
"invalid_notification_text"
|
|
)
|
|
if not content then
|
|
return false, content_error
|
|
end
|
|
|
|
SendNUIMessage({
|
|
type = "notification:show",
|
|
data = {
|
|
appId = app_id,
|
|
route = "/apps/" .. app_id,
|
|
text = content,
|
|
title = title,
|
|
},
|
|
})
|
|
return true
|
|
end
|
|
|
|
local function register_high_server_app(owner_resource, definition, revision)
|
|
if type(owner_resource) ~= "string"
|
|
or type(definition) ~= "table"
|
|
or type(definition.id) ~= "string"
|
|
or type(revision) ~= "number"
|
|
or revision ~= math.floor(revision)
|
|
or revision < 1
|
|
then
|
|
print(("[%s] Rejected invalid High Phone server application snapshot."):format(RESOURCE_NAME))
|
|
return nil
|
|
end
|
|
|
|
local app_id = definition.id
|
|
local existing = high_server_apps[app_id]
|
|
if existing and revision <= existing.revision then
|
|
if revision < existing.revision or existing.registered then
|
|
return app_id
|
|
end
|
|
if existing.owner_resource ~= owner_resource then
|
|
print(("[%s] Rejected conflicting High Phone owner for %s."):format(
|
|
RESOURCE_NAME,
|
|
app_id
|
|
))
|
|
return app_id
|
|
end
|
|
end
|
|
|
|
local provider_record = provider_apps[app_id]
|
|
local success, error_message
|
|
local retained_registration = false
|
|
if provider_record then
|
|
if provider_record.owner_resource ~= owner_resource or provider_record.provider ~= providers.high then
|
|
success, error_message = false, "duplicate_app_id"
|
|
else
|
|
retained_registration = true
|
|
success, error_message = core.Update(owner_resource, definition)
|
|
end
|
|
else
|
|
success, error_message = core.Add(owner_resource, definition)
|
|
end
|
|
|
|
high_server_apps[app_id] = {
|
|
definition = definition,
|
|
last_error = success and nil or error_message,
|
|
owner_resource = owner_resource,
|
|
registered = success or retained_registration,
|
|
revision = revision,
|
|
}
|
|
if success then
|
|
provider_apps[app_id] = {
|
|
definition = definition,
|
|
owner_resource = owner_resource,
|
|
provider = providers.high,
|
|
}
|
|
elseif not existing or existing.revision ~= revision or existing.last_error ~= error_message then
|
|
print(("[%s] Could not register High Phone server application %s revision %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
app_id,
|
|
revision,
|
|
error_message or "unknown_error"
|
|
))
|
|
end
|
|
return app_id
|
|
end
|
|
|
|
local function remove_high_server_app(owner_resource, app_id)
|
|
local record = high_server_apps[app_id]
|
|
if not record or record.owner_resource ~= owner_resource then
|
|
return
|
|
end
|
|
|
|
high_server_apps[app_id] = nil
|
|
if record.registered then
|
|
core.Remove(owner_resource, app_id)
|
|
end
|
|
|
|
local client_record = high_client_apps[app_id]
|
|
if client_record then
|
|
local success, error_message = register_provider_app(
|
|
providers.high,
|
|
client_record.owner_resource,
|
|
client_record.definition,
|
|
nil
|
|
)
|
|
if not success then
|
|
print(("[%s] Could not restore High Phone client application %s: %s."):format(
|
|
RESOURCE_NAME,
|
|
app_id,
|
|
error_message or "unknown_error"
|
|
))
|
|
end
|
|
end
|
|
end
|
|
|
|
RegisterNetEvent("sky_phone:compat:high:client:syncApplication", function(owner_resource, definition, revision)
|
|
if source ~= 65535 then
|
|
print(("[%s] Rejected locally invoked High Phone application sync."):format(RESOURCE_NAME))
|
|
return
|
|
end
|
|
register_high_server_app(owner_resource, definition, revision)
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:compat:high:client:removeApplication", function(owner_resource, app_id)
|
|
if source ~= 65535 then
|
|
print(("[%s] Rejected locally invoked High Phone application removal."):format(RESOURCE_NAME))
|
|
return
|
|
end
|
|
if type(owner_resource) ~= "string" or type(app_id) ~= "string" then
|
|
print(("[%s] Rejected invalid High Phone application removal."):format(RESOURCE_NAME))
|
|
return
|
|
end
|
|
remove_high_server_app(owner_resource, app_id)
|
|
end)
|
|
|
|
RegisterNetEvent("sky_phone:compat:high:client:replaceSnapshot", function(snapshot)
|
|
if source ~= 65535 then
|
|
print(("[%s] Rejected locally invoked High Phone application snapshot."):format(RESOURCE_NAME))
|
|
return
|
|
end
|
|
if type(snapshot) ~= "table" then
|
|
print(("[%s] Rejected invalid High Phone application snapshot."):format(RESOURCE_NAME))
|
|
return
|
|
end
|
|
|
|
local seen = {}
|
|
for index = 1, #snapshot do
|
|
local record = snapshot[index]
|
|
if type(record) == "table" then
|
|
local app_id = register_high_server_app(
|
|
record.owner_resource,
|
|
record.definition,
|
|
record.revision
|
|
)
|
|
if app_id then
|
|
seen[app_id] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local removed = {}
|
|
for app_id, record in pairs(high_server_apps) do
|
|
if not seen[app_id] then
|
|
removed[#removed + 1] = {
|
|
app_id = app_id,
|
|
owner_resource = record.owner_resource,
|
|
}
|
|
end
|
|
end
|
|
for index = 1, #removed do
|
|
remove_high_server_app(removed[index].owner_resource, removed[index].app_id)
|
|
end
|
|
end)
|
|
|
|
AddEventHandler("onClientResourceStart", function(resource_name)
|
|
for _, record in pairs(high_server_apps) do
|
|
if record.owner_resource == resource_name and not record.registered then
|
|
register_high_server_app(record.owner_resource, record.definition, record.revision)
|
|
end
|
|
end
|
|
end)
|
|
|
|
AddEventHandler("onClientResourceStop", function(resource_name)
|
|
for app_id, record in pairs(high_client_apps) do
|
|
if record.owner_resource == resource_name then
|
|
high_client_apps[app_id] = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
exports("AddApplication", add_17mov_application)
|
|
exports("RemoveApplication", remove_17mov_application)
|
|
exports("SendAppMessage", send_app_message)
|
|
exports("addApplication", add_high_application)
|
|
exports("sendAppNui", send_high_app_nui)
|
|
exports("addCustomApp", add_quasar_app)
|
|
exports("addCustomAppsBatch", add_quasar_apps_batch)
|
|
exports("updateCustomApp", update_quasar_app)
|
|
exports("removeCustomApp", remove_quasar_app)
|
|
exports("getCustomApps", get_quasar_apps)
|
|
exports("OpenPhoneApp", open_quasar_app)
|
|
exports("OpenApp", open_phone_app)
|
|
exports("CloseApp", close_phone_app)
|
|
exports("SendNotification", send_lb_notification)
|
|
|
|
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "OpenApp", open_phone_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "CloseApp", close_phone_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "SendNotification", send_lb_notification)
|
|
|
|
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "AddApplication", add_17mov_application)
|
|
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "RemoveApplication", remove_17mov_application)
|
|
SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "SendAppMessage", send_app_message)
|
|
|
|
SkyPhoneCompatibility.RegisterExportAlias("high-phone", "addApplication", add_high_application)
|
|
SkyPhoneCompatibility.RegisterExportAlias("high-phone", "sendAppNui", send_high_app_nui)
|
|
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomApp", add_quasar_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomAppsBatch", add_quasar_apps_batch)
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "updateCustomApp", update_quasar_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "removeCustomApp", remove_quasar_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "getCustomApps", get_quasar_apps)
|
|
SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "OpenPhoneApp", open_quasar_app)
|
|
|
|
SkyPhoneCompatibility.RegisterExportAlias("yseries", "SendAppMessage", send_app_message)
|
|
SkyPhoneCompatibility.RegisterExportAlias("yseries", "CloseApp", close_phone_app)
|
|
SkyPhoneCompatibility.RegisterExportAlias("yseries", "GetDataLoaded", function()
|
|
return true
|
|
end)
|
|
|
|
CreateThread(function()
|
|
TriggerServerEvent("sky_phone:compat:high:server:requestSnapshot")
|
|
TriggerEvent("17mov_Phone:Client:Ready")
|
|
|
|
local provider_resources = {
|
|
"lb-phone",
|
|
"17mov_Phone",
|
|
"high-phone",
|
|
"qs-smartphone",
|
|
"yseries",
|
|
}
|
|
for index = 1, #provider_resources do
|
|
TriggerEvent("onResourceStart", provider_resources[index])
|
|
end
|
|
end)
|