+
+
diff --git a/resources/[sonorancad]/sonorancad/core/client_nui/js/http.js b/resources/[sonorancad]/sonorancad/core/client_nui/js/http.js
new file mode 100644
index 000000000..205b3fccd
--- /dev/null
+++ b/resources/[sonorancad]/sonorancad/core/client_nui/js/http.js
@@ -0,0 +1,7 @@
+$(function () {
+ window.addEventListener('message', function (event) {
+ if (event.data.type == "light_event") {
+ $.post("http://localhost:" + event.data.port + "/lighting", JSON.stringify({ state: event.data.event }))
+ }
+ });
+});
\ No newline at end of file
diff --git a/resources/[sonorancad]/sonorancad/core/client_nui/sounds/beeps.mp3 b/resources/[sonorancad]/sonorancad/core/client_nui/sounds/beeps.mp3
new file mode 100644
index 000000000..915f2b964
Binary files /dev/null and b/resources/[sonorancad]/sonorancad/core/client_nui/sounds/beeps.mp3 differ
diff --git a/resources/[sonorancad]/sonorancad/core/commands.lua b/resources/[sonorancad]/sonorancad/core/commands.lua
new file mode 100644
index 000000000..9d93cb19f
--- /dev/null
+++ b/resources/[sonorancad]/sonorancad/core/commands.lua
@@ -0,0 +1,245 @@
+--[[
+ SonoranCAD FiveM Integration
+
+ Commands Module
+
+ Provides /sonoran command for console control
+]]
+
+--[[ /sonoran
+ debugmode - old caddebug toggle
+ info - dump version info, configuration
+ support - dump useful data for support staff
+ verify - run hash checks to confirm all files are untampered
+ plugin
+
+[](https://travis-ci.org/petkaantonov/bluebird)
+[](http://petkaantonov.github.io/bluebird/coverage/debug/index.html)
+
+**Got a question?** Join us on [stackoverflow](http://stackoverflow.com/questions/tagged/bluebird), the [mailing list](https://groups.google.com/forum/#!forum/bluebird-js) or chat on [IRC](https://webchat.freenode.net/?channels=#promises)
+
+# Introduction
+
+Bluebird is a fully featured promise library with focus on innovative features and performance
+
+See the [**bluebird website**](http://bluebirdjs.com/docs/getting-started.html) for further documentation, references and instructions. See the [**API reference**](http://bluebirdjs.com/docs/api-reference.html) here.
+
+For bluebird 2.x documentation and files, see the [2.x tree](https://github.com/petkaantonov/bluebird/tree/2.x).
+
+# Questions and issues
+
+The [github issue tracker](https://github.com/petkaantonov/bluebird/issues) is **_only_** for bug reports and feature requests. Anything else, such as questions for help in using the library, should be posted in [StackOverflow](http://stackoverflow.com/questions/tagged/bluebird) under tags `promise` and `bluebird`.
+
+
+
+## Thanks
+
+Thanks to BrowserStack for providing us with a free account which lets us support old browsers like IE8.
+
+# License
+
+The MIT License (MIT)
+
+Copyright (c) 2013-2016 Petka Antonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/resources/[sonorancad]/sonorancad/node_modules/bluebird/changelog.md b/resources/[sonorancad]/sonorancad/node_modules/bluebird/changelog.md
new file mode 100644
index 000000000..73b2eb6c7
--- /dev/null
+++ b/resources/[sonorancad]/sonorancad/node_modules/bluebird/changelog.md
@@ -0,0 +1 @@
+[http://bluebirdjs.com/docs/changelog.html](http://bluebirdjs.com/docs/changelog.html)
diff --git a/resources/[sonorancad]/sonorancad/node_modules/bluebird/js/browser/bluebird.core.js b/resources/[sonorancad]/sonorancad/node_modules/bluebird/js/browser/bluebird.core.js
new file mode 100644
index 000000000..ffb653822
--- /dev/null
+++ b/resources/[sonorancad]/sonorancad/node_modules/bluebird/js/browser/bluebird.core.js
@@ -0,0 +1,3739 @@
+/* @preserve
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2015 Petka Antonov
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+/**
+ * bluebird build version 3.4.7
+ * Features enabled: core
+ * Features disabled: race, call_get, generators, map, nodeify, promisify, props, reduce, settle, some, using, timers, filter, any, each
+*/
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Promise=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof _dereq_=="function"&&_dereq_;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof _dereq_=="function"&&_dereq_;for(var o=0;oPlayer ID: %s
Name: %s
Date of Birth: %s
"):format(char.img, target, name, dob), + type = "success", + layout = "bottomcenter", + timeout = "10000" + }) + end + end + end) + end) + + if pluginConfig.allowCustomIds then + RegisterCommand("setid", function(source, args, rawCommand) + TriggerClientEvent("SonoranCAD::civintegration:SetCustomId", source) + end) + + RegisterCommand("resetid", function(source, args, rawCommand) + if CustomCharacterCache[source] ~= nil then + CustomCharacterCache[source] = nil + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^2OK ^0] ", "Custom character removed."}}) + end + end) + + RegisterNetEvent("SonoranCAD::civintegration:SetCustomId") + AddEventHandler("SonoranCAD::civintegration:SetCustomId", function(id) + CustomCharacterCache[source] = {{ ['first'] = id.first, ['last'] = id.last, ['dob'] = id.dob, img = "https://sonorancad.com/statics/images/blank_user.jpg" }} + end) + end + + if pluginConfig.allowPurge then + RegisterCommand("refreshid", function(source, args, rawCommand) + CharacterCacheTimers[source] = 0 + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^2OK ^0] ", "Reset character list. Use /showid again."}}) + end) + end + end + + +end \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/cl_dispatchnotify.lua b/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/cl_dispatchnotify.lua new file mode 100644 index 000000000..072cd4d1f --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/cl_dispatchnotify.lua @@ -0,0 +1,157 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: dispatchnotify + Creator: SonoranCAD + Description: Show incoming 911 calls and allow units to attach to them. + + Put all client-side logic in this file. +]] + +local trackingCall = false +local trackingID = nil + +CreateThread(function() Config.LoadPlugin("dispatchnotify", function(pluginConfig) + + if pluginConfig.enabled then + + local gpsLock = true + local lastPostal = nil + local lastCoords = nil + local currentCallId = nil + local lockedPlate = nil + local gpsBlip = false + + RegisterNetEvent("SonoranCAD::dispatchnotify:SetGps") + AddEventHandler("SonoranCAD::dispatchnotify:SetGps", function(postal) + -- try to set postal via command? + if gpsLock then + ExecuteCommand("postal "..tostring(postal)) + if lastPostal ~= nil and lastPostal ~= postal then + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", ("Call GPS coordinates updated (%s)."):format(postal)}}) + lastPostal = postal + else + lastPostal = postal + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", ("GPS coordinates set to caller's last known postal (%s)."):format(postal)}}) + end + end + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:UnsetGps") + AddEventHandler("SonoranCAD::dispatchnotify:UnsetGps", function() + if gpsBlip then + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", "You are now on scene. Disabling GPS."}}) + RemoveBlip(gpsBlip) + gpsBlip = nil + elseif lastPostal ~= nil then + ExecuteCommand("postal") + lastPostal = nil + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", "You are now on scene. Disabling GPS."}}) + end + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:SetLocation") + AddEventHandler("SonoranCAD::dispatchnotify:SetLocation", function(coords) + if coords == nil then + return warnLog("SetLocation was called, but no coordinates were found") + else + debugLog(("In SetLocation: x: %s y: %s z: %s"):format(coords.x, coords.y, coords.z)) + end + if gpsLock then + if gpsBlip then RemoveBlip(gpsBlip) end + gpsBlip = AddBlipForCoord(tonumber(coords.x), tonumber(coords.y), 0.0) + SetBlipRouteColour(gpsBlip, 3) + SetBlipRoute(gpsBlip, true) + if lastCoords ~= nil then + if lastCoords.x == coords.x and lastCoords.y == coords.y then + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", "GPS coordinates have been updated."}}) + return + end + end + lastCoords = coords + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Dispatch ^0] ", "GPS coordinates set to caller's last known location."}}) + end + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:BeginTracking") + AddEventHandler("SonoranCAD::dispatchnotify:BeginTracking", function(callID) + trackingCall = true + trackingID = callID + track() + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:StopTracking") + AddEventHandler("SonoranCAD::dispatchnotify:StopTracking", function() + trackingCall = false + trackingID = nil + end) + + RegisterCommand("togglegps", function(source, args, rawCommand) + gpsLock = not gpsLock + TriggerEvent("chat:addMessage", {args = {"^0[ ^2GPS ^0] ", ("GPS lock has been %s"):format(gpsLock and "enabled" or "disabled")}}) + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:CallAttach") + RegisterNetEvent("SonoranCAD::dispatchnotify:CallDetach") + RegisterNetEvent("SonoranCAD::dispatchnotify:AddNoteToCall") + + AddEventHandler("SonoranCAD::dispatchnotify:CallAttach", function(callId) + debugLog("Got attach for call "..tostring(callId)) + currentCallId = callId + end) + AddEventHandler("SonoranCAD::dispatchnotify:CallDetach", function(callId) + debugLog("Got detach for call "..tostring(callId)) + currentCallId = nil + if gpsBlip then RemoveBlip(gpsBlip) end + gpsBlip = nil + end) + + function track() + local lastpostal = nil + if trackingCall then + while trackingCall and trackingID ~= nil do + local postal = nil + if isPluginLoaded("postals") and getNearestPostal() ~= nil then + postal = getNearestPostal() + else + assert(false, "Required postal resource is not loaded. Cannot use postals plugin.") + end + if postal ~= nil and postal ~= lastpostal then + TriggerServerEvent("SonoranCAD::dispatchnotify:UpdateCallPostal", postal, trackingID) + lastpostal = postal + end + Citizen.Wait(pluginConfig.postalSendTimer) + end + end + end + + if pluginConfig.enableAddNote then + RegisterCommand(pluginConfig.addNoteCommand, function(source, args, rawCommand) + local note = table.concat(args, " ") + if currentCallId ~= nil then + TriggerServerEvent("SonoranCAD::dispatchnotify:AddNoteToCall", currentCallId, note) + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Note ^0] ", "Note sent to CAD."}}) + else + TriggerEvent("chat:addMessage", {args = {"^0[ ^4Error ^0] ", "Not attached to any call."}}) + end + end) + end + + if pluginConfig.enableAddPlate and isPluginLoaded("wraithv2") then + RegisterNetEvent("SonoranCAD::dispatchnotify:PlateLock") + AddEventHandler("SonoranCAD::dispatchnotify:PlateLock", function(plate) + debugLog("Got locked plate event "..tostring(plate)) + lockedPlate = plate + end) + RegisterCommand(pluginConfig.addPlateCommand, function(source, args, rawCommand) + if currentCallId ~= nil and lockedPlate ~= nil then + TriggerServerEvent("SonoranCAD::dispatchnotify:AddNoteToCall", currentCallId, ("PLATE NUMBER: %s"):format(lockedPlate)) + TriggerEvent("chat:addMessage", {args = {"^0[ ^2Note ^0] ", ("Locked plate %s sent to CAD."):format(lockedPlate)}}) + else + TriggerEvent("chat:addMessage", {args = {"^0[ ^4Error ^0] ", "Not attached to any call or no plate locked."}}) + end + end) + end + + end +end) end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/sv_dispatchnotify.lua b/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/sv_dispatchnotify.lua new file mode 100644 index 000000000..806818466 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/dispatchnotify/sv_dispatchnotify.lua @@ -0,0 +1,558 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: dispatchnotify + Creator: SonoranCAD + Description: Show incoming 911 calls and allow units to attach to them. + + Put all server-side logic in this file. +]] + +CreateThread(function() Config.LoadPlugin("dispatchnotify", function(pluginConfig) + +if pluginConfig.enabled then + + local DISPATCH_TYPE = {"CALL_NEW", "CALL_EDIT", "CALL_CLOSE", "CALL_NOTE", "CALL_SELF_CLEAR"} + local ORIGIN = {"CALLER", "RADIO_DISPATCH", "OBSERVED", "WALK_UP"} + local STATUS = {"PENDING", "ACTIVE", "CLOSED"} + + local CallOriginMapping = {} -- callId => playerId + local EmergencyToCallMapping = {} -- eCallId => CallId + local CallNotes = {} -- callid -> notes table + + local MappedCalls = {} -- eCallId -> call object + + local function findCall(id) + for idx, callId in pairs(EmergencyToCallMapping) do + debugLog(("check %s = %s"):format(id, callId)) + if id == callId then + return idx + end + end + return nil + end + + local function getCallFromOriginId(id) + for k, call in pairs(GetCallCache()) do + if call.dispatch ~= nil then + if call.dispatch.metaData ~= nil then + debugLog(("check %s = %s"):format(id, call.dispatch.metaData.createdFromId)) + if tonumber(call.dispatch.metaData.createdFromId) == tonumber(id) then + return call + end + end + end + end + return nil + end + + local function SendMessage(type, source, message) + debugLog(("Sending message to %s: %s"):format(source, message)) + if type == "dispatch" then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^2Dispatch ^0] ", message}}) + elseif type == pluginConfig.emergencyCallType then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^1"..type.." ^0] ", message}}) + elseif type == pluginConfig.civilCallType then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^1"..type.." ^0] ", message}}) + elseif type == pluginConfig.dotCallType then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^1"..type.." ^0] ", message}}) + elseif type == "error" then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^1Error ^0] ", message}}) + elseif type == "debug" and Config.debugMode then + TriggerClientEvent("chat:addMessage", source, {args = {"[ Debug ] ", message}}) + end + end + + local function IsPlayerOnDuty(player) + if pluginConfig.unitDutyMethod == "incad" then + if GetUnitByPlayerId(tostring(player)) ~= nil then + return true + else + return false + end + elseif pluginConfig.unitDutyMethod == "permissions" then + return IsPlayerAceAllowed(player, "sonorancad.dispatchnotify") + elseif pluginConfig.unitDutyMethod == "esxjob" then + assert(isPluginLoaded("esxsupport") or isPluginLoaded("frameworksupport"), "frameworksupport plugin is required to use the esx/qb-core on duty method.") + local job = GetCurrentJob(player) + debugLog(("Player %s has job %s, return %s"):format(player, job, pluginConfig.esxJobsAllowed[GetCurrentJob(player)] )) + if pluginConfig.esxJobsAllowed[GetCurrentJob(player)] then + return true + else + return false + end + elseif pluginConfig.unitDutyMethod == "custom" then + return unitDutyCustom(player) + end + end + + local function addCallNote(callId, note) + if not CallNotes[callId] then + local tbl = {} + table.insert(tbl, note) + CallNotes[callId] = tbl + else + table.insert(CallNotes[callId], note) + end + end + + local function clearNotes(callId) + CallNotes[callId] = nil + end + + local ActiveDispatchers = {} + + AddEventHandler("SonoranCAD::pushevents:UnitLogin", function(unit) + if unit.isDispatch and pluginConfig.dispatchDisablesSelfResponse then + pluginConfig.enableUnitResponse = false + debugLog("Self dispatching disabled, dispatch is online") + table.insert(ActiveDispatchers, unit.id) + end + end) + + AddEventHandler("SonoranCAD::pushevents:UnitLogout", function(id) + local idx = nil + for i, k in pairs(ActiveDispatchers) do + if id == k then + idx = i + end + end + if idx ~= nil then + table.remove(ActiveDispatchers, idx) + end + if pluginConfig.dispatchDisablesSelfResponse and #ActiveDispatchers < 1 then + pluginConfig.enableUnitResponse = true + debugLog("Self dispatching enabled, dispatch is offline") + end + end) + + --EVENT_911 TriggerEvent('SonoranCAD::pushevents:IncomingCadCall', body.data.call, body.data.apiIds, body.data.metaData) + RegisterServerEvent("SonoranCAD::pushevents:IncomingCadCall") + AddEventHandler("SonoranCAD::pushevents:IncomingCadCall", function(call, metadata, apiIds) + if metadata ~= nil and metadata.callerPlayerId ~= nil then + CallOriginMapping[call.callId] = metadata.callerPlayerId + end + if pluginConfig.enableUnitNotify then + local type = call.emergency and pluginConfig.civilCallType or pluginConfig.emergencyCallType + local message = pluginConfig.incomingCallMessage:gsub("{caller}", call.caller):gsub("{location}", call.location):gsub("{description}", call.description):gsub("{callId}", call.callId):gsub("{command}", pluginConfig.respondCommandName) + for i = 0, GetNumPlayerIndices()-1 do + local player = GetPlayerFromIndex(i) + local unit = GetUnitByPlayerId(player) + if IsPlayerOnDuty(player) then + if pluginConfig.unitNotifyMethod == "chat" then + SendMessage(type, player, message) + elseif pluginConfig.unitNotifyMethod == "pnotify" then + TriggerClientEvent("pNotify:SendNotification", player, { + text = message, + type = "error", + layout = "bottomcenter", + timeout = "10000" + }) + elseif pluginConfig.unitNotifyMethod == "custom" then + TriggerClientEvent("SonoranCAD::dispatchnotify:IncomingCallNotify", player, message) + end + else + debugLog(("Ignore player %s, not on duty"):format(player)) + end + end + end + end) + + RegisterServerEvent("SonoranCAD::callcommands:EmergencyCallAdd") + AddEventHandler("SonoranCAD::callcommands:EmergencyCallAdd", function(playerId, callId) + CallOriginMapping[tonumber(callId)] = playerId + end) + + --Officer response + registerApiType("NEW_DISPATCH", "emergency") + registerApiType("ATTACH_UNIT", "emergency") + registerApiType("REMOVE_911", "emergency") + registerApiType("SET_CALL_POSTAL", "emergency") + RegisterCommand(pluginConfig.respondCommandName, function(source, args, rawCommand) + local source = tonumber(source) + local callId = args[1] + if callId == nil then + SendMessage("error", source, "Call ID must be specified.") + return + end + callId = tonumber(callId) + if not pluginConfig.enableUnitResponse then + SendMessage("error", source, "Self dispatching is disabled.") + return + end + if not IsPlayerOnDuty(source) then + SendMessage("error", source, "You must be on duty to use this command.") + return + end + if not GetUnitByPlayerId(source) then + SendMessage("error", source, "Due to system limitations, you must be logged into the CAD to self attach.") + return + end + + -- Fetch if call hasn't been responded to yet + local call = GetEmergencyCache()[callId] + if call == nil then + -- Call responded, grab from mapping + call = MappedCalls[callId] + end + if call == nil then + -- not in mapping, check call cache + call = getCallFromOriginId(callId) + end + if call == nil then + SendMessage("error", source, "Could not find that call ID") + return + elseif call.dispatch ~= nil then + call = call.dispatch + end + local callerPlayerId = nil + local originCall = nil + if call.metaData ~= nil then + callerPlayerId = call.metaData.callerPlayerId + originCall = call.metaData.createdFromId + end + if call.metaData ~= nil and callerPlayerId == nil then + debugLog("failed to find caller info") + end + local identifiers = GetIdentifiers(source)[Config.primaryIdentifier] + if originCall == nil then + -- no mapped call, create a new one + debugLog(("Creating new call request...(no mapped call for %s)"):format(callId)) + local postal = "" + if isPluginLoaded("postals") and callerPlayerId ~= nil then + if PostalsCache[tonumber(callerPlayerId)] ~= nil then + postal = PostalsCache[tonumber(callerPlayerId)] + else + debugLog("Failed to obtain postal. "..json.encode(PostalsCache)) + return + end + end + if call.metaData ~= nil and call.metaData.useCallLocation == "true" and call.metaData.callPostal ~= nil then + postal = call.metaData.callPostal + end + local title = "OFFICER RESPONSE - "..call.callId + if pluginConfig.callTitle ~= nil then + title = pluginConfig.callTitle.." - "..call.callId + end + metaData = {callerPlayerId = callerPlayerId, createdFromId = call.callId } + if call.metaData ~= nil then + for k, v in pairs(call.metaData) do + metaData[k] = v + end + end + if LocationCache[source] ~= nil and metaData['x'] == nil then + metaData['x'] = LocationCache[source].coordinates.x + metaData['y'] = LocationCache[source].coordinates.y + metaData['z'] = LocationCache[source].coordinates.z + end + local payload = { serverId = Config.serverId, + origin = 0, + status = 1, + priority = 2, + block = "", + code = "", + postal = (postal ~= nil and postal or ""), + address = (call.location ~= nil and call.location or "Unknown"), + title = title, + description = (call.description ~= nil and call.description or ""), + isEmergency = call.isEmergency, + notes = { + {time = '00:00:00', label = 'Dispatch', type = 'text', content = 'Officer Responding'} + }, + metaData = metaData, + units = { identifiers } + } + performApiRequest({payload}, "NEW_DISPATCH", function(response) + debugLog("Call creation OK") + if response:match("NEW DISPATCH CREATED - ID:") then + TriggerEvent("SonoranCAD::dispatchnotify:UnitRespond", source, response:match("%d+")) + EmergencyToCallMapping[call.callId] = tonumber(response:match("%d+")) + end + -- remove the 911 call + local payload = { serverId = Config.serverId, callId = call.callId } + performApiRequest({payload}, "REMOVE_911", function(resp) + debugLog("Remove status: "..tostring(resp)) + end) + end) + else + -- Call already exists + debugLog("Found Call. Attaching!") + local data = {callId = call.callId, units = {identifiers}, serverId = Config.serverId} + performApiRequest({data}, "ATTACH_UNIT", function(res) + debugLog("Attach OK: "..tostring(res)) + SendMessage("debug", source, "You have been attached to the call.") + end) + end + end) + + RegisterNetEvent("SonoranCAD::dispatchnotify:CallAttach") + RegisterNetEvent("SonoranCAD::dispatchnotify:CallDetach") + RegisterServerEvent("SonoranCAD::pushevents:UnitAttach") + AddEventHandler("SonoranCAD::pushevents:UnitAttach", function(call, unit) + debugLog("hello, unit attach! "..json.encode(call)) + local callerId = nil + if call.dispatch.metaData ~= nil and call.dispatch.metaData.callerPlayerId ~= nil then + debugLog("set caller ID "..call.dispatch.metaData.callerPlayerId) + callerId = call.dispatch.metaData.callerPlayerId + end + local officerId = GetSourceByApiId(unit.data.apiIds) + if officerId ~= nil then + SendMessage("dispatch", officerId, ("You are now attached to call ^4%s^0. Description: ^4%s^0"):format(call.dispatch.callId, call.dispatch.description)) + TriggerClientEvent("SonoranCAD::dispatchnotify:CallAttach", officerId, call.dispatch.callId) + local callerLocation = nil + if callerId ~= nil then + callerLocation = findPlayerLocation(callerId) + end + if callerLocation == nil or call.dispatch.metaData.useCallLocation then + callerLocation = {x=call.dispatch.metaData.x, y=call.dispatch.metaData.y, z=call.dispatch.metaData.z} + end + debugLog(("Sending location data %s to %s (call data: %s)"):format(json.encode(callerLocation), officerId, json.encode(call))) + if pluginConfig.waypointType == "exact" and callerLocation ~= nil then + TriggerClientEvent("SonoranCAD::dispatchnotify:SetLocation", officerId, callerLocation) + elseif pluginConfig.waypointType == "postal" or pluginConfig.waypointFallbackEnabled then + if call.dispatch.postal ~= nil and call.dispatch.postal ~= "" then + TriggerClientEvent("SonoranCAD::dispatchnotify:SetGps", officerId, call.dispatch.postal) + if call.dispatch.metaData ~= nil and call.dispatch.metaData.trackPrimary == "True" then + if GetSourceByApiId(GetUnitCache()[call.dispatch.idents[1]].data.apiIds) == officerId then + TriggerClientEvent("SonoranCAD::dispatchnotify:BeginTracking", officerId, call.dispatch.callId) + end + end + end + else + local lc = LocationCache[callerId] + if lc == nil then + lc = { ['error'] = "locationcache is nil"} + end + debugLog(("LOCATION SETTING: Failed to send client location. - waypointType: %s - callerId: %s - LocationCache: %s"):format(pluginConfig.waypointType, callerId, json.encode(lc))) + end + else + debugLog("failed to find unit "..json.encode(unit)) + end + if pluginConfig.enableCallerNotify and callerId ~= nil and call.dispatch.metaData.silentAlert == "false" then + if pluginConfig.callerNotifyMethod == "chat" then + SendMessage("dispatch", callerId, pluginConfig.notifyMessage:gsub("{officer}", unit.data.name)) + elseif pluginConfig.callerNotifyMethod == "pnotify" then + TriggerClientEvent("pNotify:SendNotification", callerId, { + text = pluginConfig.notifyMessage:gsub("{officer}", unit.data.name), + type = "error", + layout = "bottomcenter", + timeout = "10000" + }) + elseif pluginConfig.callerNotifyMethod == "custom" then + TriggerEvent("SonoranCAD::dispatchnotify:UnitAttach", call.dispatch, callerId, officerId, unit.data.name) + end + else + debugLog(("pluginConfig.enableCallerNotify == %s and %s ~= nil and not %s == 'false'"):format(pluginConfig.enableCallerNotify, callerId, call.dispatch.metaData.silentAlert)) + end + end) + + RegisterServerEvent("SonoranCAD::pushevents:DispatchEvent") + AddEventHandler("SonoranCAD::pushevents:DispatchEvent", function(data) + local dispatchType = data.dispatch_type + local dispatchData = data.dispatch + local metaData = data.dispatch.metaData + if dispatchType ~= tostring(dispatchType) then + -- hmm, expected a string, got a number + dispatchType = DISPATCH_TYPE[data.dispatch_type+1] + end + local switch = { + ["CALL_NEW"] = function() + debugLog("CALL_NEW fired "..json.encode(dispatchData)) + local emergencyId = dispatchData.metaData.createdFromId + for k, id in pairs(dispatchData.idents) do + local unit = GetUnitCache()[GetUnitById(id)] + if not unit then + debugLog("Not sending attach, unit not online") + else + local officerId = GetSourceByApiId(unit.data.apiIds) + TriggerEvent("SonoranCAD::pushevents:UnitAttach", data, unit) + end + end + end, + ["CALL_CLOSE"] = function() + debugLog("CALL_CLOSE fired "..json.encode(dispatchData)) + if dispatchData == nil or dispatchData.dispatch == nil then + debugLog("nil value detected, ignore it") + return + end + local cache = GetCallCache()[dispatchData.dispatch.callId] + if cache.units ~= nil then + for k, v in pairs(cache.units) do + local officerId = GetUnitCache()[GetUnitById(v.id)] + if officerId ~= nil then + TriggerClientEvent("SonoranCAD::dispatchnotify:CallClosed", officerId, cache.callId) + end + end + end + clearNotes(dispatchData.dispatch.callId) + end, + ["CALL_NOTE"] = function() + TriggerEvent("SonoranCAD::dispatchnotify:CallNote", dispatchData.callId, dispatchData.notes) + end, + ["CALL_SELF_CLEAR"] = function() + TriggerEvent("SonoranCAD::dispatchnotify:CallSelfClear", dispatchData.units) + end + } + if switch[dispatchType] then + switch[dispatchType]() + end + end) + + AddEventHandler("SonoranCAD::pushevents:DispatchEdit", function(before, after) + if before.dispatch.primary ~= after.dispatch.primary then + -- Primary Unit Updated, remove tracking from old unit. + local unit = GetUnitCache()[GetUnitById(before.dispatch.primary)] + if unit ~= nil then + local officerId = GetSourceByApiId(unit.data.apiIds) + TriggerClientEvent("SonoranCAD::dispatchnotify:StopTracking", officerId) + end + end + if before.dispatch.primary ~= after.dispatch.primary or before.dispatch.trackPrimary ~= after.dispatch.trackPrimary then + TriggerEvent("SonoranCAD::dispatchnotify:CallEdit:Tracking", after.dispatch.callId, after.dispatch.trackPrimary, after.dispatch.primary) + end + if before.dispatch.postal ~= after.dispatch.postal then + TriggerEvent("SonoranCAD::dispatchnotify:CallEdit:Postal", after.dispatch.callId, after.dispatch.postal) + end + if before.address ~= after.address then + TriggerEvent("SonoranCAD::dispatchnotify:CallEdit:Address", after.dispatch.callId, after.dispatch.address) + end + end) + + AddEventHandler("SonoranCAD::dispatchnotify:CallEdit:Tracking", function(callId, tracking, primary) + local call = GetCallCache()[callId] + assert(call ~= nil, "Call not found, failed to process.") + local unit = GetUnitCache()[GetUnitById(primary)] + local officerId = GetSourceByApiId(unit.data.apiIds) + if tracking then + TriggerClientEvent("SonoranCAD::dispatchnotify:BeginTracking", officerId, callId) + else + TriggerClientEvent("SonoranCAD::dispatchnotify:StopTracking", officerId) + end + end) + + + AddEventHandler("SonoranCAD::pushevents:UnitDetach", function(call, unit) + local officerId = GetSourceByApiId(unit.data.apiIds) + if GetCallCache()[call.dispatch.callId] == nil then + debugLog("Ignore unit detach, call doesn't exist") + return + end + if officerId ~= nil and call ~= nil and call.dispatch.metaData ~= nil then + if call.dispatch.metaData.trackPrimary then + TriggerClientEvent("SonoranCAD::dispatchnotify:StopTracking", officerId) + end + TriggerClientEvent("SonoranCAD::dispatchnotify:CallDetach", officerId, call.dispatch.callId) + SendMessage("dispatch", officerId, ("You were detached from call %s."):format(call.dispatch.callId)) + end + end) + + AddEventHandler("SonoranCAD::dispatchnotify:CallEdit:Postal", function(callId, postal) + local call = GetCallCache()[callId] + assert(call ~= nil, "Call not found, failed to process.") + if call.dispatch.idents == nil then + debugLog("no units attached "..json.encode(call)) + return + end + for k, id in pairs(call.dispatch.idents) do + local unit = GetUnitCache()[GetUnitById(id)] + if unit == nil then + debugLog(("Unit was nil, requested %s, cache is: %s"):format(id, GetUnitCache())) + return + end + local officerId = GetSourceByApiId(unit.data.apiIds) + if officerId ~= nil then + TriggerClientEvent("SonoranCAD::dispatchnotify:SetGps", officerId, postal) + else + debugLog("couldn't find officer") + end + end + end) + RegisterServerEvent("SonoranCAD::dispatchnotify:UpdateCallPostal") + AddEventHandler("SonoranCAD::dispatchnotify:UpdateCallPostal", function(clpostal, callid) + local data = {} + data[1] = { + callId = callid, + postal = clpostal, + serverId = Config.serverId + } + performApiRequest(data, 'SET_CALL_POSTAL', function() end) + end) + + AddEventHandler("SonoranCAD::pushevents:DispatchNote", function(call, data) + if not pluginConfig.sendNotesToUnits then + return + end + if not call then + debugLog(("Failed to find call: %s"):format(json.encode(data))) + return + end + call = call.dispatch + -- add note to cache + addCallNote(data.callId, data.note) + debugLog(("Incoming note for ID %s, call: %s"):format(data.callId, json.encode(call))) + local noteContent = type(data.note) == 'table' and data.note.content or data.note + if call.idents ~= nil and type(noteContent) == 'string' then + for _, ident in pairs(call.idents) do + local officerId = GetUnitCache()[GetUnitById(ident)] + if officerId ~= nil then + local patterns = { ["{callid}"] = data.callId, ["{note}"] = noteContent} + local message = pluginConfig.noteMessage + for k, v in pairs(patterns) do + message = message:gsub(k, v) + end + if pluginConfig.noteNotifyMethod == "chat" then + SendMessage("dispatch", officerId, message) + elseif pluginConfig.noteNotifyMethod == "pnotify" then + TriggerClientEvent("pNotify:SendNotification", officerId, { + text = message, + type = "info", + layout = "bottomcenter", + timeout = "10000" + }) + else + TriggerClientEvent("SonoranCAD::dispatchnotify:NewCallNote", officerId, data) + end + else + debugLog(("Skipping officer %s, not available"):format(ident)) + end + end + end + end) + + registerApiType("ADD_CALL_NOTE", "emergency") + RegisterNetEvent("SonoranCAD::dispatchnotify:AddNoteToCall") + AddEventHandler("SonoranCAD::dispatchnotify:AddNoteToCall", function(callId, note) + local source = source + debugLog(("Got note add request from %s, call id %s: %s"):format(source, callId, note)) + local call = GetCallCache()[callId] + if call == nil then + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^2Dispatch ^0] ", "Unable to find call."}}) + else + local payload = { serverId = Config.serverId, note = note, callId = callId } + performApiRequest({payload}, "ADD_CALL_NOTE", function(res) end) + end + end) + if isPluginLoaded("wraithv2") then + AddEventHandler("wk:onPlateLocked", function(cam, plate, index) + local plate = plate:match("^%s*(.-)%s*$") + if IsPlayerOnDuty(source) then + TriggerClientEvent("SonoranCAD::dispatchnotify:PlateLock", source, plate) + end + end) + else + debugLog("Not loading radar lock as wraith plugin is not loaded") + end + + AddEventHandler("SonoranCAD::pushevents:UnitUpdate", function(unit, status) + local u = GetUnitCache()[unit] + if u then + local player = GetSourceByApiId(u.data.apiIds) + if player then + TriggerClientEvent("SonoranCAD::dispatchnotify:UnsetGps", player) + end + end + end) + +end + +end) end) diff --git a/resources/[sonorancad]/sonorancad/submodules/fivepd/cl_fivepd.lua b/resources/[sonorancad]/sonorancad/submodules/fivepd/cl_fivepd.lua new file mode 100644 index 000000000..10e649907 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/fivepd/cl_fivepd.lua @@ -0,0 +1,17 @@ +--[[ + Sonaran CAD Plugins + Plugin Name: fivepd + Creator: SonoranCAD + Description: Callouts and Record Sync with FivePD +]] + +CreateThread(function() + Config.LoadPlugin("fivepd", function(pluginConfig) + + if pluginConfig.enabled then + + + end + + end) +end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/fivepd/put_in_fivepd_plugins/SonoranPlugin.net.dll b/resources/[sonorancad]/sonorancad/submodules/fivepd/put_in_fivepd_plugins/SonoranPlugin.net.dll new file mode 100644 index 000000000..c0e230e71 Binary files /dev/null and b/resources/[sonorancad]/sonorancad/submodules/fivepd/put_in_fivepd_plugins/SonoranPlugin.net.dll differ diff --git a/resources/[sonorancad]/sonorancad/submodules/fivepd/sv_fivepd.lua b/resources/[sonorancad]/sonorancad/submodules/fivepd/sv_fivepd.lua new file mode 100644 index 000000000..b5faaf9e2 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/fivepd/sv_fivepd.lua @@ -0,0 +1,73 @@ +--[[ + Sonaran CAD Plugins + Plugin Name: fivepd + Creator: SonoranCAD + Description: Callouts and Record Sync with FivePD +]] + +CreateThread(function() + Config.LoadPlugin("fivepd", function(pluginConfig) + if pluginConfig.enabled then + local postalsConfig = Config.GetPluginConfig('postals') + registerApiType("NEW_DISPATCH", "emergency") + + -- New Callout Handler + function CreateNewCallout(src, callName, callDesc, callResponse, callLocation, callCoord) + local identifier = GetIdentifiers(src)[Config.primaryIdentifier] + local units = {identifier} + local notes = "" + local postal = "" + if postalsConfig and postalsConfig.enabled then + postal = getPostalFromVector3(callCoord) or "" + end + + local data = { + ['serverId'] = Config.serverId, + ['origin'] = pluginConfig.origin, + ['status'] = pluginConfig.status, + ['priority'] = callResponse, + ['block'] = "", -- not used, but required + ['postal'] = postal, + ['address'] = callLocation ~= nil and callLocation or 'Unknown', + ['title'] = callName, + ['code'] = pluginConfig.code, -- TODO + ['description'] = callDesc, + ['units'] = units, + ['notes'] = {} -- required but empty + } + + debugLog("Sending New Callout") + performApiRequest({data}, 'NEW_DISPATCH', function() end) + end + + RegisterServerEvent("SonoranCAD::fivepd:CalloutReceived", function(src, callIdent, callId, callName, callDesc, callResponse, callLocX, callLocY, callLocZ) + -- This Event doesn't seem to trigger so I didn't use it. + end) + RegisterServerEvent("SonoranCAD::fivepd:CalloutAccepted", function(src, callIdent, callId, callName, callDesc, callResponse, callLocation, callCoord) + CreateNewCallout(src, callName, callDesc, callResponse, callLocation, callCoord) + end) + RegisterServerEvent("SonoranCAD::fivepd:CalloutCompleted", function(src, callIdent, callId, callName, callDesc, callResponse, callLocX, callLocY, callLocZ) + print(src .. " completed callout: " .. json.encode(callout)) + + end) + RegisterServerEvent("SonoranCAD::fivepd:DutyStatusChange", function(src, onDuty) + print(src .. " is on duty: " .. tostring(onDuty)) + + end) + RegisterServerEvent("SonoranCAD::fivepd:ServiceCalled", function(src, service) + print(src .. " called for: " .. tostring(service)) + + end) + RegisterServerEvent("SonoranCAD::fivepd:RankChanged", function(src, rank) + print(src .. " is now rank: " .. tostring(rank)) + + end) + RegisterServerEvent("SonoranCAD::fivepd:PedArrested", function(src, pedData) + print(src .. " arrested ped: " .. tostring(pedData.FirstName) .. " " .. tostring(pedData.LastName)) + + end) + + end + + end) +end) diff --git a/resources/[sonorancad]/sonorancad/submodules/forcereg/cl_forcereg.lua b/resources/[sonorancad]/sonorancad/submodules/forcereg/cl_forcereg.lua new file mode 100644 index 000000000..76ab52c58 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/forcereg/cl_forcereg.lua @@ -0,0 +1,139 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: forcereg + Creator: Era#1337 + Description: Requires players to link their API IDs to a valid Sonoran account. + +]] + +local pluginConfig = Config.GetPluginConfig("forcereg") + +if pluginConfig.enabled then + + local isNagging = false + local isFreezing = false + local freezePos = nil + local isNoSpawn = false + local id = nil + local idString = nil + + RegisterNetEvent("SonoranCAD::forcereg:PlayerReg") + AddEventHandler("SonoranCAD::forcereg:PlayerReg", function(identifier, exists) + if not exists then + Wait(1) + id = identifier + idString = identifier + if isPluginLoaded("esxsupport") then + if Config.plugins.esxsupport.usePrefix then + idString = ("%s:%s"):format(Config.primaryIdentifier, identifier) + else + idString = identifier + end + end + print(("Identifier %s does not exist."):format(idString)) + if pluginConfig.captiveOption:lower() == "nag" then + isNagging = true + elseif pluginConfig.captiveOption:lower() == "freeze" then + isFreezing = true + elseif pluginConfig.captiveOption:lower() == "nospawn" then + isNoSpawn = true + else + assert(false, 'Invalid captiveOption!') + end + else + print("Identity verified.") + isNagging = false + isFreezing = false + freezePos = nil + isNoSpawn = false + end + end) + + TriggerServerEvent("SonoranCAD::forcereg:CheckPlayer") + + RegisterCommand("verifycad", function(source, args, rawCommand) + TriggerServerEvent("SonoranCAD::forcereg:CheckPlayer") + end) + + CreateThread(function() + while true do + if isNagging then + -- USER CONFIG: Change the below to adjust the text to your liking + if pluginConfig.nagDrawTextLocation:lower() == "top" then + DrawText2D(pluginConfig.captiveMessage, 0, 0, 0.305, 0.01, 0.3, 255, 255, 255, 150) + DrawText2D(pluginConfig.instructionalMessage, 0, 0, 0.3, 0.03, 0.3, 255, 255, 255, 150) + DrawText2D(pluginConfig.verifyMessage.." API ID: ~r~"..id, 0, 0, 0.35, 0.06, 0.3, 255, 255, 255, 150) + elseif pluginConfig.nagDrawTextLocation:lower() == "center" then + DrawText2D(pluginConfig.captiveMessage, 0, 0, 0.2, 0.4, 0.5, 255, 255, 255, 150) + DrawText2D(pluginConfig.instructionalMessage, 0, 0, 0.195, 0.45, 0.5, 255, 255, 255, 150) + DrawText2D(pluginConfig.verifyMessage.." API ID: ~r~"..idString, 0, 0, 0.265, 0.5, 0.5, 255, 255, 255, 150) + end + -- END USER CONFIG + Wait(0) + elseif isFreezing then + CreateThread(function() + while isFreezing do + if freezePos == nil then + freezePos = GetEntityCoords(PlayerPedId()) + end + FreezeEntityPosition(PlayerPedId(), true) + ClearPedTasksImmediately(PlayerPedId()) + SetEntityCoords(PlayerPedId(), freezePos, 0.0, 0.0, 0.0, false) + + -- USER CONFIG: Change the below to adjust the text to your liking + DrawText2D(pluginConfig.captiveMessage, 0, 0, 0.2, 0.4, 0.5, 255, 255, 255, 150) + DrawText2D(pluginConfig.instructionalMessage, 0, 0, 0.195, 0.45, 0.5, 255, 255, 255, 150) + DrawText2D(pluginConfig.verifyMessage.." API ID: ~r~"..idString, 0, 0, 0.265, 0.5, 0.5, 255, 255, 255, 150) + -- END USER CONFIG + Wait(0) + end + FreezeEntityPosition(PlayerPedId(), false) + freezePos = nil + end) + Wait(1000) + while freezePos ~= nil do + Wait(10) + end + + elseif isNoSpawn then + -- do nothing, for now + else + Wait(100) + end + end + end) + +end + + +-- utility + +local AspectRatio +local ScreenWidth +local ScreenHeight + +Citizen.CreateThread(function() + AspectRatio = GetAspectRatio(false) + ScreenWidth = 1080 * AspectRatio + ScreenHeight = 1080 +end) + +function DrawText2D(text, font, centre, px, py, scale, r, g, b, a, labelGen) + if labelGen then + AddTextEntry(labelGen, text) + end + SetTextFont(font) + SetTextProportional(0) + SetTextScale(scale, scale) + SetTextColour(r or 255, g or 255, b or 255, a or 255) + SetTextDropShadow(0, 0, 0, 0,255) + SetTextEdge(1, 0, 0, 0, 255) + --SetTextOutline() + SetTextCentre(centre) + SetTextEntry(labelGen or "STRING") + AddTextComponentString(text) + local x = px + (scale / 2.0) / ScreenWidth + local y = py + (scale / 2.0) / ScreenHeight + DrawText(x, y) +end diff --git a/resources/[sonorancad]/sonorancad/submodules/forcereg/sv_forcereg.lua b/resources/[sonorancad]/sonorancad/submodules/forcereg/sv_forcereg.lua new file mode 100644 index 000000000..906fce3fa --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/forcereg/sv_forcereg.lua @@ -0,0 +1,103 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: forcereg + Creator: Era#1337 + Description: Requires players to link their API IDs to a valid Sonoran account. + +]] + +local pluginConfig = Config.GetPluginConfig("forcereg") + +if pluginConfig.enabled then + + if pluginConfig.captiveOption == "whitelist" then + local function checkApiId(apiId, deferral, cb) + cadApiIdExists(apiId, function(exists) + debugLog(("checkApiId %s"):format(exists)) + cb(exists, deferral) + end) + end + + AddEventHandler("playerConnecting", function(name, setMessage, deferrals) + local source = source + deferrals.defer() + Wait(1) + deferrals.update("Checking CAD account, please wait...") + checkApiId(GetIdentifiers(source)[Config.primaryIdentifier], deferrals, function(exists, deferral) + print("exists: "..tostring(exists)) + if not exists then + deferral.done(pluginConfig.captiveMessage) + else + deferral.done() + end + end) + end) + end + + + + RegisterNetEvent("SonoranCAD::forcereg:CheckPlayer") + AddEventHandler("SonoranCAD::forcereg:CheckPlayer", function() + TriggerEvent("SonoranCAD::apicheck:CheckPlayerLinked", source) + end) + + AddEventHandler("SonoranCAD::apicheck:CheckPlayerLinkedResponse", function(player, identifier, exists) + if not pluginConfig.whitelist then + pluginConfig.whitelist = { + enabled = false, + mode = "qb-core", -- qb-core, esx, ace + aces = { -- ace permissions will see the message + "forcereg.whitelist" + }, + jobs = { -- QB or ESX jobs will see the message + "police" + } + } + print("Forcereg: Whitelist configuration not found, using defaults. Please update your configuration.") + end + if pluginConfig.whitelist.enabled then + if pluginConfig.whitelist.mode == "ace" then + local aceAllowed = false + for i=1, #pluginConfig.whitelist.aces do + if IsPlayerAceAllowed(player, pluginConfig.whitelist.aces[i]) then + aceAllowed = true + break + end + end + if aceAllowed then + TriggerClientEvent("SonoranCAD::forcereg:PlayerReg", player, identifier, exists) + end + elseif pluginConfig.whitelist.mode == "qb-core" then + local QBCore = exports['qb-core']:GetCoreObject() + local Player = QBCore.Functions.GetPlayer(player) + local job = Player.PlayerData.job.name + if job ~= nil then + for i=1, #pluginConfig.whitelist.jobs do + if job == pluginConfig.whitelist.jobs[i] then + TriggerClientEvent("SonoranCAD::forcereg:PlayerReg", player, identifier, exists) + break + end + end + end + elseif pluginConfig.whitelist.mode == "esx" then + local ESX = exports['es_extended']:getSharedObject() + local xPlayer = ESX.GetPlayerFromId(player) + local job = xPlayer.job.name + if job ~= nil then + for i=1, #pluginConfig.whitelist.jobs do + if job == pluginConfig.whitelist.jobs[i] then + TriggerClientEvent("SonoranCAD::forcereg:PlayerReg", player, identifier, exists) + break + end + end + end + end + else + TriggerClientEvent("SonoranCAD::forcereg:PlayerReg", player, identifier, exists) + end + end) + + + +end \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/frameworksupport/cl_frameworksupport.lua b/resources/[sonorancad]/sonorancad/submodules/frameworksupport/cl_frameworksupport.lua new file mode 100644 index 000000000..a6a9db3f0 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/frameworksupport/cl_frameworksupport.lua @@ -0,0 +1,108 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: frameworksupport + Creator: Sonoran Software Systems LLC + Description: Enable using ESX or QBCore character information in Sonoran integration plugins +]] CreateThread(function() + Config.LoadPlugin('frameworksupport', function(pluginConfig) + + if pluginConfig.enabled then + CreateThread(function() + local QBCore = nil + if pluginConfig.usingQBCore then + QBCore = exports['qb-core']:GetCoreObject() + end + PlayerData = {} + local ESX = nil + + CreateThread(function() + if not pluginConfig.usingQBCore then + ESX = exports['es_extended']:getSharedObject() + end + if pluginConfig.usingQBCore then + while QBCore.Functions.GetPlayerData() == nil do + Wait(10) + end + PlayerData = QBCore.Functions.GetPlayerData() + else + while ESX.GetPlayerData() == nil do + Wait(10) + end + PlayerData = ESX.GetPlayerData() + end + end) + + -- Listen for when new players load into the game + RegisterNetEvent('esx:playerLoaded') + AddEventHandler('esx:playerLoaded', function(xPlayer) + if pluginConfig.usingQBCore then + PlayerData = QBCore.Functions.GetPlayerData() + else + PlayerData = xPlayer + end + end) + -- Listen for when jobs are changed in esx_jobs + if pluginConfig.usingQBCore then + RegisterNetEvent('QBCore:Client:OnJobUpdate') + AddEventHandler('QBCore:Client:OnJobUpdate', function(job) + PlayerData.job = job + if PlayerData.job.onduty == true then + PlayerData.job.name = 'offduty' .. PlayerData.job.name + end + TriggerServerEvent('SonoranCAD::frameworksupport:refreshJobCache') + TriggerEvent('SonoranCAD::frameworksupport:JobUpdate', job) + end) + else + RegisterNetEvent('esx:setJob') + AddEventHandler('esx:setJob', function(job) + PlayerData.job = job + TriggerServerEvent('SonoranCAD::frameworksupport:refreshJobCache') + TriggerEvent('SonoranCAD::frameworksupport:JobUpdate', job) + end) + end + -- QBUS onduty change (ESX typically uses jobs to change duty instead) + if pluginConfig.usingQBCore then + RegisterNetEvent('QBCore:Client:SetDuty') + AddEventHandler('QBCore:Client:SetDuty', function(onduty) + local job = PlayerData.job + if onduty then + job.name = string.gsub(job.name, 'offduty', '') + else + job.name = 'offduty' .. job.name + end + PlayerData.job = job + TriggerServerEvent('SonoranCAD::frameworksupport:refreshJobCache') + TriggerEvent('SonoranCAD::frameworksupport:JobUpdate', job) + end) + end + + -- Function to return esx_identity data on the client from server + -- This event listens for data from the server when requested + local recievedIdentity = false + returnedIdentity = nil + RegisterNetEvent('SonoranCAD::frameworksupport:returnIdentity') + AddEventHandler('SonoranCAD::frameworksupport:returnIdentity', function(data) + recievedIdentity = true + if data.job == nil then + warnLog('Warning: no identity data was found.') + else + returnedIdentity = data + end + end) + -- This function requests data from the server + function GetIdentity(callback) + recievedIdentity = false + returnIdentity = false + TriggerServerEvent('SonoranCAD::frameworksupport:getIdentity') + local timeStamp = GetGameTimer() + while not recievedIdentity do + Wait(0) + end + callback(returnedIdentity) + end + + end) + end + end) +end) diff --git a/resources/[sonorancad]/sonorancad/submodules/frameworksupport/sv_frameworksupport.lua b/resources/[sonorancad]/sonorancad/submodules/frameworksupport/sv_frameworksupport.lua new file mode 100644 index 000000000..b36689816 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/frameworksupport/sv_frameworksupport.lua @@ -0,0 +1,368 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: frameworksupport + Creator: Sonoran Software Systems LLC + Description: Enable using ESX (or ESX clones) character information in Sonoran integration plugins +]] CreateThread(function() + Config.LoadPlugin('frameworksupport', function(pluginConfig) + + if pluginConfig.enabled then + + if GetResourceState('qb-core') ~= "started" and GetResourceState('es_extended') ~= 'started' then + errorLog('Both qb-core and es_extended are not started. Disabling framework support.') + pluginConfig.enabled = false + pluginConfig.disableReason = 'qb-core and es_extended not started' + return + end + local QBCore = nil + local ESX = nil + if pluginConfig.usingQBCore then + if GetResourceState('qb-core') ~= "started" then + errorLog('qb-core is not started. Disabling framework support.') + pluginConfig.enabled = false + pluginConfig.disableReason = 'qb-core not started' + return + end + QBCore = exports['qb-core']:GetCoreObject() + end + if not pluginConfig.usingQBCore then + if GetResourceState('es_extended') ~= 'started' then + errorLog('es_extended is not started. Disabling framework support.') + pluginConfig.enabled = false + pluginConfig.disableReason = 'es_extended not started' + return + end + ESX = exports['es_extended']:getSharedObject() + end + JobCache = {} + + RegisterNetEvent('QBCore:Client:OnPlayerLoaded') + AddEventHandler('QBCore:Client:OnPlayerLoaded', function() + playerName = QBCore.Functions.GetPlayerData() + end) + + -- Legacy ESX helper functions to get Character Info using MySQL-Async + local function safeParameters(params) + if nil == params then + return {[''] = ''} + end + + assert(type(params) == 'table', 'A table is expected') + assert(params[1] == nil, 'Parameters should not be an array, but a map (key / value pair) instead') + + if next(params) == nil then + return {[''] = ''} + end + + return params + end + + function MysqlAsyncFetchAll(query, params, func) + assert(type(query) == 'string', 'The SQL Query must be a string') + + exports['mysql-async']:mysql_fetch_all(query, safeParameters(params), func) + end + + function MysqlSyncFetchAll(query, params) + assert(type(query) == 'string', 'The SQL Query must be a string') + + local res = {} + local finishedQuery = false + exports['mysql-async']:mysql_fetch_all(query, safeParameters(params), function(result) + res = result + finishedQuery = true + end) + repeat + Wait(0) + until finishedQuery == true + return res + end + + function GetLegacyCharInfo(target, callback) + local identifier = GetPlayerIdentifiers(target)[1] + local result = MysqlSyncFetchAll('SELECT * FROM `users` WHERE `identifier` = @identifier', {['@identifier'] = identifier}) + if result[1]['firstname'] ~= nil then + local data = {identifier = result[1]['identifier'], firstname = result[1]['firstname'], lastname = result[1]['lastname'], dateofbirth = result[1]['dateofbirth'], sex = result[1]['sex'], + height = result[1]['height']} + callback(data) + else + local data = {identifier = identifier, firstname = '', lastname = '', dateofbirth = '', sex = '', height = ''} + callback(data) + end + end + + -- Helper function to get the ESX Identity object from your database/framework + function GetIdentity(target, cb) + local xPlayer = nil + if pluginConfig.usingQBCore then + xPlayer = QBCore.Functions.GetPlayer(target) + else + xPlayer = ESX.GetPlayerFromId(target) + end + if xPlayer ~= nil then + debugLog('GetIdentity OK') + if pluginConfig.usingQBCore then + xPlayer.firstName = xPlayer.PlayerData.charinfo.firstname + xPlayer.lastName = xPlayer.PlayerData.charinfo.lastname + xPlayer.name = xPlayer.firstName .. ' ' .. xPlayer.lastName + elseif pluginConfig.legacyESX then + -- Get Char info from Database using MySQL-Async + GetLegacyCharInfo(target, function(data) + -- debug logging for lookups that find no user data in database + if data.firstname == '' then + debugLog('Legacy ESX database lookup found no user data for identifier: ' .. tostring(data.identifier)) + end + -- Set quick reference variables + xPlayer.firstname = data.firstname + xPlayer.lastName = data.lastname + xPlayer.name = data.firstname .. ' ' .. data.lastname + -- Adjust xPlayer.getName() + xPlayer.getName = function() + return xPlayer.name + end + end) + end + if cb ~= nil then + debugLog('Running callback') + cb(xPlayer) + else + debugLog('Running client event') + TriggerClientEvent('SonoranCAD::frameworksupport:returnIdentity', target, xPlayer) + end + else + debugLog('GetIdentity Failed') + if cb ~= nil then + cb({}) + else + TriggerClientEvent('SonoranCAD::frameworksupport:returnIdentity', target, {}) + end + end + end + + -- Helper function that just returns the current job as a callback + function GetCurrentJob(player, cb) + local currentJob = '' + if cb == nil then + if JobCache[tostring(player)] ~= nil then + debugLog('Return cached player') + return JobCache[tostring(player)] + else + debugLog(('Player %s has no cached job'):format(player)) + end + end + local xPlayer = nil + if pluginConfig.usingQBCore then + xPlayer = QBCore.Functions.GetPlayer(tonumber(player)) + else + xPlayer = ESX.GetPlayerFromId(player) + end + if xPlayer == nil then + warnLog(('Failed to obtain player info from %s. ESX.GetPlayerFromId returned nil.'):format(player)) + else + if pluginConfig.usingQBCore then + if not xPlayer.PlayerData.job.onduty then -- QBUS job.onduty is false when on duty??? okayyyyy + currentJob = xPlayer.PlayerData.job.name + else + currentJob = 'offduty' .. xPlayer.PlayerData.job.name + end + else + currentJob = xPlayer.job.name + end + debugLog('Returned job: ' .. tostring(currentJob)) + end + if cb == nil then + JobCache[tostring(player)] = currentJob + return currentJob + elseif cb == true then + JobCache[tostring(player)] = currentJob + debugLog('refreshed job cache for player ' .. player .. '-' .. currentJob) + else + cb(currentJob) + end + end + + -- Caching functionality, used locally to reduce database load + CreateThread(function() + while ESX == nil do + Wait(10) + end + local xPlayers = nil + if pluginConfig.usingQBCore then + xPlayers = QBCore.Functions.GetPlayers() + else + xPlayers = ESX.GetPlayers() + end + for i = 1, #xPlayers, 1 do + local player = nil + if pluginConfig.usingQBCore then + player = QBCore.Functions.GetPlayers(tonumber(xPlayers[i])) + else + player = ESX.GetPlayerFromId(xPlayers[i]) + end + if player == nil then + debugLog('Failed to obtain job from player ' .. tostring(xPlayers[i])) + else + if pluginConfig.usingQBCore then + if not player.PlayerData.job.onduty then + JobCache[tostring(player)] = player.PlayerData.job.name + else + JobCache[tostring(player)] = 'offduty' .. player.PlayerData.job.name + end + else + JobCache[tostring(player)] = player.job.name + end + end + end + Wait(30000) + end) + + AddEventHandler('playerDropped', function() + JobCache[tostring(source)] = nil + end) + + -- Event for clients to request esx_identity information from the server + RegisterNetEvent('SonoranCAD::frameworksupport:getIdentity') + AddEventHandler('SonoranCAD::frameworksupport:getIdentity', function() + GetIdentity(source) + end) + + -- Event for clients to trigger job refresh on server (primarily for QBUS onduty handling) + RegisterNetEvent('SonoranCAD::frameworksupport:refreshJobCache') + AddEventHandler('SonoranCAD::frameworksupport:refreshJobCache', function() + local src = source + GetCurrentJob(src, true) + end) + + -- EVENT_RECORD_ADDED + RegisterServerEvent('SonoranCAD::pushevents:RecordAdded') + AddEventHandler('SonoranCAD::pushevents:RecordAdded', function(record) + -- Check to see if we should be issuing fines. + if not pluginConfig.issueFines then + return + end + debugLog('Receieved new record') + + local isFineable = false + for _, formName in pairs(pluginConfig.fineableForms) do + if record.name:upper() == formName:upper() then + isFineable = true + end + end + if isFineable then + -- Create empty citation object + local citation = {issuer = nil, -- Issuer of the fine + first = nil, -- First name of the fine target + last = nil, -- Last name of the fine target + fine = 0, -- Total sum of all fineable offenses + department = nil} + debugLog(record.name:upper() .. ' is a fineable record.') + -- Iterate the sections of the record + for k, sec in pairs(record.sections) do + -- Iterate the fields of the record section + for _, field in pairs(sec.fields) do + -- Store the first name of the fine target + if field.uid == 'first' then + citation.first = field.value + end + -- Store the last name of the fine target + if field.uid == 'last' then + citation.last = field.value + end + -- Retrieve the new Unit Name from the Agency Information + if field.type == 'UNIT_NAME' then + citation.issuer = field.value + end + -- Get "Special" fields from the report + if field.type == 'UNIT_DEPARTMENT' then + citation.department = field.value + end + if field.label == 'New Field Name' then + if field.data then + -- Get and store the name of the issuing officer to the citation + if field.data.officer then + citation.issuer = field.data.officer + end + -- Get and add speeding charges to the citation + if field.data.fine then + citation.fine = citation.fine + tonumber(field.data.fine) + debugLog('Added fine of $' .. field.data.fine .. ' for ' .. field.data.vehicleSpeed .. ' in a ' .. field.data.speedLimit .. 'zone.') + end + -- Get and add other charges to the citation + if field.data.charges then + for _, charge in pairs(field.data.charges) do + local fineTotal = tonumber(charge.arrestBondAmount) * charge.arrestChargeCounts + citation.fine = citation.fine + tonumber(fineTotal) + debugLog('Added fine of $' .. fineTotal .. ' for ' .. charge.arrestChargeCounts .. ' counts of ' .. charge.arrestCharge) + end + end + end + end + end + end + + debugLog('New Citation to Issue:') + debugLog('Issuer: ' .. tostring(citation.issuer)) + debugLog('Issued To: ' .. tostring(citation.first) .. ' ' .. tostring(citation.last)) + debugLog('Total Fines: $' .. tostring(citation.fine)) + + -- If the citation is missing a first name or a last name we can't issue the fine. + if citation.first == '' or citation.last == '' then + return + end + + -- Find the civilian that matches the citation and issue them a fine. + if pluginConfig.usingQBCore then + xPlayers = QBCore.Functions.GetPlayers() + else + xPlayers = ESX.GetPlayers() + end + + for i = 1, #xPlayers, 1 do + GetIdentity(xPlayers[i], function(xPlayer) + if pluginConfig.usingQBCore then + if xPlayer.PlayerData.charinfo.firstname == citation.first then + if xPlayer.PlayerData.charinfo.lastname == citation.last then + debugLog('found player online matching fined character') + xPlayer.Functions.RemoveMoney('bank', citation.fine) + if pluginConfig.usingQBManagement then + if pluginConfig.qbManagementAccountNames[citation.department] ~= nil then + exports['qb-management']:AddMoney(pluginConfig.qbManagementAccountNames[citation.department], citation.fine) + end + end + if pluginConfig.fineNotify then + debugLog('sending fine notification') + local finemessage = citation.first .. ' ' .. citation.last .. ' has been issued a fine of $' .. citation.fine + if citation.issuer ~= '' then + finemessage = finemessage .. ' by ' .. citation.issuer + end + TriggerClientEvent('chat:addMessage', -1, {color = {255, 0, 0}, multiline = true, args = {finemessage}}) + end + if pluginConfig.qbNotifyFinedPlayer then + TriggerClientEvent('QBCore:Notify', xPlayer.PlayerData.source, pluginConfig.qbFineMessage:gsub('$AMOUNT', citation.fine):gsub('$OFFICER_NAME', citation.issuer), 'error', 5000) + end + end + end + else + if xPlayer.getName() == citation.first .. ' ' .. citation.last then + debugLog('found player online matching fined character') + xPlayer.removeAccountMoney('bank', citation.fine) + ESX.SavePlayer(xPlayer) + if pluginConfig.fineNotify then + debugLog('sending fine notification') + local finemessage = xPlayer.getName() .. ' has been issued a fine of $' .. citation.fine + if citation.issuer ~= '' then + finemessage = finemessage .. ' by ' .. citation.issuer + end + TriggerClientEvent('chat:addMessage', -1, {color = {255, 0, 0}, multiline = true, args = {finemessage}}) + end + end + end + end) + end + end + end) + end + + end) +end) diff --git a/resources/[sonorancad]/sonorancad/submodules/kick/cl_kick.lua b/resources/[sonorancad]/sonorancad/submodules/kick/cl_kick.lua new file mode 100644 index 000000000..53481ca50 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/kick/cl_kick.lua @@ -0,0 +1,15 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: kick + Creator: Taylor McGaw + Description: Kicks user from the cad upon exiting the server +]] +local pluginConfig = Config.GetPluginConfig("kick") + +if pluginConfig.enabled then +--------------------------------------------------------------------------- +-- Chat Suggestions **DO NOT EDIT UNLESS YOU KNOW WHAT YOU ARE DOING** +--------------------------------------------------------------------------- + +end \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/kick/sv_kick.lua b/resources/[sonorancad]/sonorancad/submodules/kick/sv_kick.lua new file mode 100644 index 000000000..bf348dcc3 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/kick/sv_kick.lua @@ -0,0 +1,42 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: kick + Creator: Taylor McGaw + Description: Kicks user from the cad upon exiting the server +]] + +local pluginConfig = Config.GetPluginConfig("kick") + +if pluginConfig.enabled then + + local PendingKicks = {} + registerApiType("KICK_UNIT", "emergency") + AddEventHandler("playerDropped", function() + local source = source + local identifier = GetIdentifiers(source)[Config.primaryIdentifier] + if not identifier then + debugLog("kick: no API ID, skip") + return + end + table.insert(PendingKicks, identifier) + end) + + CreateThread(function() + while true do + if #PendingKicks > 0 then + local kicks = {} + while true do + local pendingKick = table.remove(PendingKicks) + if pendingKick ~= nil then + table.insert(kicks, {["apiId"] = pendingKick, ["reason"] = "You have exited the server", ["serverId"] = Config.serverId}) + else + break + end + end + performApiRequest(kicks, 'KICK_UNIT', function() end) + end + Wait(10000) + end + end) +end \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/locations/cl_locations.lua b/resources/[sonorancad]/sonorancad/submodules/locations/cl_locations.lua new file mode 100644 index 000000000..3013a5567 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/locations/cl_locations.lua @@ -0,0 +1,82 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: locations + Creator: SonoranCAD + Description: Implements location updating for players +]] +CreateThread(function() Config.LoadPlugin("locations", function(pluginConfig) + + if pluginConfig.enabled then + + local currentLocation = '' + local lastLocation = 'none' + local lastSentTime = nil + local lastCoords = { x = 0, y = 0, z = 0 } + + local function sendLocation() + local pos = GetEntityCoords(PlayerPedId()) + local var1, var2 = GetStreetNameAtCoord(pos.x, pos.y, pos.z, Citizen.ResultAsInteger(), Citizen.ResultAsInteger()) + local postal = nil + if isPluginLoaded("postals") then + postal = getNearestPostal() + else + pluginConfig.prefixPostal = false + end + local l1 = GetStreetNameFromHashKey(var1) + local l2 = GetStreetNameFromHashKey(var2) + if l2 ~= '' then + currentLocation = l1 .. ' / ' .. l2 + else + currentLocation = l1 + end + if (bodyCamOn or currentLocation ~= lastLocation or vector3(pos.x, pos.y, pos.z) ~= vector3(lastCoords.x, lastCoords.y, lastCoords.z)) then + -- Location changed, continue + local toSend = currentLocation + if pluginConfig.prefixPostal and postal ~= nil then + toSend = "["..tostring(postal).."] "..currentLocation + elseif postal == nil and pluginConfig.prefixPostal == true then + debugLog("Unable to send postal because I got a null response from getNearestPostal()?!") + end + if bodyCamOn then + TriggerServerEvent('SonoranCAD::locations:SendLocation', toSend, pos, bodyCamFrequency) + else + TriggerServerEvent('SonoranCAD::locations:SendLocation', toSend, pos) + end + lastCoords = pos + debugLog(("Locations different, sending. (%s ~= %s) SENT: %s (POS: %s)"):format(currentLocation, lastLocation, toSend, json.encode(lastCoords))) + lastSentTime = GetGameTimer() + lastLocation = currentLocation + end + end + + Citizen.CreateThread(function() + -- Wait for plugins to settle + Wait(5000) + while true do + while not NetworkIsPlayerActive(PlayerId()) do + Wait(10) + end + sendLocation() + -- Wait (1000ms) before checking for an updated unit location + Citizen.Wait(pluginConfig.checkTime) + end + end) + + Citizen.CreateThread(function() + while lastSentTime == nil do + while not NetworkIsPlayerActive(PlayerId()) do + Wait(10) + end + Wait(15000) + if lastSentTime == nil then + TriggerServerEvent("SonoranCAD::locations:ErrorDetection", true) + warnLog("Warning: No location data has been sent yet. Check for errors.") + end + Wait(30000) + end + end) + + end + + end) end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/locations/sv_locations.lua b/resources/[sonorancad]/sonorancad/submodules/locations/sv_locations.lua new file mode 100644 index 000000000..4c20f2946 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/locations/sv_locations.lua @@ -0,0 +1,82 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: locations + Creator: SonoranCAD + Description: Implements location updating for players +]] +CreateThread(function() Config.LoadPlugin("locations", function(pluginConfig) + + if pluginConfig.enabled then + -- Pending location updates array + LocationCache = {} + local LastSend = 0 + + -- Main api POST function + local function SendLocations() + while true do + local cache = {} + for k, v in pairs(LocationCache) do + if v.isUpdated ~= nil then + v.isUpdated = nil + table.insert(cache, v) + end + end + if #cache > 0 then + if GetGameTimer() > LastSend+5000 then + performApiRequest(cache, 'UNIT_LOCATION', function() end) + LastSend = GetGameTimer() + else + debugLog(("UNIT_LOCATION: Attempted to send data too soon. %s !> %s"):format(GetGameTimer(), LastSend+5000)) + end + end + Wait(Config.postTime+500) + end + end + + function findPlayerLocation(playerSrc) + if LocationCache[playerSrc] ~= nil then + return LocationCache[playerSrc].location + end + return nil + end + + -- Main update thread sending api location update POST requests per the postTime interval + Citizen.CreateThread(function() + Wait(1) + SendLocations() + end) + + -- Event from client when location changes occur + RegisterServerEvent('SonoranCAD::locations:SendLocation') + AddEventHandler('SonoranCAD::locations:SendLocation', function(currentLocation, position, bodycamFrequency) + local source = source + local identifier = GetIdentifiers(source)[Config.primaryIdentifier] + if identifier == nil then + debugLog(("user %s has no identifier for %s, skipped."):format(source, Config.primaryIdentifier)) + return + end + if bodycamFrequency then + local frameNumber = latestFrame[source] + LocationCache[source] = {['apiId'] = identifier, ['location'] = currentLocation, ['coordinates'] = position, ['isUpdated'] = true, ['bodyFrequency'] = bodycamFrequency, ['proxyUrl'] = Config.proxyUrl, ['bodyFrame'] = frameNumber} + else + LocationCache[source] = {['apiId'] = identifier, ['location'] = currentLocation, ['coordinates'] = position, ['isUpdated'] = true} + end + end) + + AddEventHandler("playerDropped", function() + local source = source + LocationCache[source] = nil + end) + + RegisterNetEvent("SonoranCAD::locations:ErrorDetection") + AddEventHandler("SonoranCAD::locations:ErrorDetection", function(isInitial) + if isInitial then + errorLog(("Player %s reported an error sending initial location data. Check client logs for errors. Did you set up the postals plugin correctly?"):format(source)) + else + warnLog(("Player %s reported an error sending location data. Check client logs for errors."):format(source)) + end + end) + + end + end) end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/lookups/sv_lookups.lua b/resources/[sonorancad]/sonorancad/submodules/lookups/sv_lookups.lua new file mode 100644 index 000000000..6ea50616a --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/lookups/sv_lookups.lua @@ -0,0 +1,303 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: lookups + Creator: SonoranCAD + Description: Implements the name/plate lookup API +]] + +local pluginConfig = Config.GetPluginConfig("lookups") + +if pluginConfig.enabled then + + registerApiType("LOOKUP", "general") + + local LookupCache = {} + + local Lookup = { + first = nil, + last = nil, + mi = nil, + plate = nil, + types = nil, + lastFetched = nil + } + function Lookup.Create(first, last, mi, plate, types, response) + local self = shallowcopy(Lookup) + self.first = first + self.last = last + self.mi = mi + self.plate = plate + self.types = types + self.lastFetched = GetGameTimer() + self.response = response + return self + end + function Lookup:UpdateCache() + self.response = info + self.lastFetched = GetGameTimer() + end + function Lookup:IsMatch(first, last, mi, plate, types) + if self.first == first and self.last == last and self.mi == mi and self.plate == plate then + for _, v in pairs(self.types) do + local match = false + for __, v2 in pairs(types) do + if v2 == v then + match = true + end + end + if not match then + return false + end + end + return true + else + return false + end + end + + -- Stale lookup garbage collector + local function PurgeStaleLookups() + local currentTime = GetGameTimer() + for k, v in pairs(LookupCache) do + local garbageTime = v.lastFetched + (pluginConfig.maxCacheTime*1000) + if currentTime >= garbageTime then + LookupCache[k] = nil + debugPrint(("Stale lookup purged %s"):format(k)) + end + end + SetTimeout(pluginConfig.stalePurgeTimer*1000, PurgeStaleLookups) + end + + PurgeStaleLookups() + + function cadLookup(data, callback, autoLookup) + -- check if the lookupData has all required fields + data["first"] = data["first"] == nil and "" or data["first"] + data["mi"] = data["mi"] == nil and "" or data["mi"] + data["last"] = data["last"] == nil and "" or data["last"] + data["plate"] = data["plate"] == nil and "" or data["plate"]:match("^%s*(.-)%s*$") + data["types"] = data["types"] == nil and {2,3,4,5} or data["types"] + + if data.first == "" and data.last == "" and data.mi == "" and data.plate == "" then + --not a valid request, just return a blank lookup + debugLog("Invalid lookup, all blanks? Trace: "..debug.traceback()) + callback({}) + return + end + if autoLookup ~= nil then + data["apiId"] = autoLookup + else + for k, v in pairs(LookupCache) do + if v:IsMatch(data.first, data.last, data.mi, data.plate, data.types) then + debugLog("Returning cached response") + callback(json.decode(v.response)) + return + end + end + end + performApiRequest({data}, "LOOKUP", function(result) + debugLog("Performed lookup") + local lookup = json.decode(result) + local l = Lookup.Create(data.first, data.last, data.mi, data.plate, data.types, result) + table.insert(LookupCache, l) + callback(lookup) + end) + + end + + function cadLookupInt(searchType, value, types, callback, autoLookup) + + end + + --[[ + cadNameLookup + first: First Name + last: Last Name + mi: Middle Initial + callback: function called with return data + ]] + function cadNameLookup(first, last, mi, callback) + local data = {} + data.first = first + data.last = last + data.mi = mi + cadLookup(data, callback, autoLookup) + end + + --[[ + cadPlateLookup + plate: plate number + basicFlag: deprecated + callback: the function called with the return data + autoLookup: when populated with an API ID, pops open a search window on the officer's CAD (optional) + ]] + function cadPlateLookup(plate, basicFlag, callback, autoLookup) + local data = {} + data["plate"] = plate + if autoLookup ~= nil then + data["apiId"] = autoLookup + end + cadLookup(data, callback, autoLookup) + + end + + function cadGetInformation(plate, callback, autoLookup) + local data = {} + data["plate"] = plate + if autoLookup ~= nil then + data["apiId"] = autoLookup + end + cadLookup(data, function(result) + local regData = {} + local charData = {} + local vehData = {} + local boloData = {} + local warrantData = {} + if result ~= nil then + for k, v in pairs(result) do + for _, record in pairs(v.sections) do + if v.type == 5 then + debugLog("Record type 5") + -- detect fields to find registration info + for k, field in pairs(record.fields) do + if field.uid == "status" and field.type == "select" then + debugLog("Found registration data") + local reg = {} + for k, field in pairs(record.fields) do + if field["uid"] ~= nil then + if string.match(field.uid, "_") then + reg[field.label:lower()] = field.value + debugLog(("set %s = %s"):format(field.label:lower(), field.value)) + else + reg[field.uid] = field.value + debugLog(("set %s = %s"):format(field.uid, field.value)) + end + end + end + table.insert(regData, reg) + elseif field.uid == "first" then + debugLog("found civilian info") + local char = {} + for _, field in pairs(record.fields) do + if field["uid"] ~= nil then + if string.match(field.uid, "_") then + char[field.label:lower()] = field.value + else + char[field.uid] = field.value + end + end + end + table.insert(charData, char) + elseif field.uid == "plate" then + debugLog("found vehicle info") + local veh = {} + for _, field in pairs(record.fields) do + if field["uid"] ~= nil then + if string.match(field.uid, "_") then + veh[field.label:lower()] = field.value + else + veh[field.uid] = field.value + end + end + end + table.insert(vehData, veh) + end + end + elseif v.type == 3 then + local boloActive = true + for _, section in pairs(v.sections) do + for _, field in pairs(section.fields) do + if field.uid == "status" then + debugLog(("Found BOLO status field %s with value %s"):format(field.label, field.value)) + if field.value == "0" then + boloActive = true + elseif field.value == "1" then + boloActive = false + end + end + end + if section.category == 1 then-- flags + if section.fields.data ~= nil and section.fields.data.flags ~= nil then + boloData = section.fields.data.flags + else + boloData = {"BOLO"} + end + end + end + if boloActive and (boloData == nil or #boloData == 0) then + boloData = {"BOLO"} + end + if not boloActive then + debugLog("BOLO inactive, mark as such") + boloData = {} + end + elseif v.type == 2 then + local warrantActive = true + for _, section in pairs(v.sections) do + for _, field in pairs(section.fields) do + if field.uid == "status" then + debugLog(("Found Warrant status field %s with value %s"):format(field.label, field.value)) + if field.value == "0" then + warrantActive = true + elseif field.value == "1" then + warrantActive = false + end + end + end + if section.category == 1 then-- flags + if section.fields[1].data ~= nil and section.fields[1].data.flags ~= nil then + warrantData = section.fields[1].data.flags + else + warrantData = {"Warrant"} + end + end + end + if warrantActive and (warrantData == nil or #warrantData == 0) then + warrantData = {"Warrant"} + end + if not warrantActive then + debugLog("Warrant inactive, mark as such") + warrantData = {} + end + end + end + end + end + callback(regData, vehData, charData, boloData, warrantData) + end, autoLookup) + end + + exports('cadNameLookup', cadNameLookup) + exports('cadPlateLookup', cadPlateLookup) + + -- The follow two commands are for developer use to analyze API responses + + RegisterCommand("platefind", function(source, args, rawCommand) + if args[1] ~= nil then + cadGetInformation(args[1], function(regData, vehData, charData, boloData) + for _, veh in pairs(vehData) do + if veh.plate:lower() == args[1]:lower() then + reg = veh + print("Got registration data "..veh.plate) + print(json.encode(veh)) + print(json.encode(regData)) + break + end + end + end) + end + end, true) + + RegisterCommand("namefind", function(source, args, rawCommand) + if args[1] ~= nil then + local firstName = args[1] + local lastName = args[2] ~= nil and args[2] or "" + local mi = args[3] ~= nil and args[3] or "" + cadNameLookup(firstName, lastName, mi, function(data) + print(("Raw data: %s"):format(json.encode(data))) + end) + end + end, true) + +end \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/postals/cl_postals.lua b/resources/[sonorancad]/sonorancad/submodules/postals/cl_postals.lua new file mode 100644 index 000000000..66eb2d967 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/postals/cl_postals.lua @@ -0,0 +1,79 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: postals + Creator: SonoranCAD + Description: Fetches nearest postal from client +]] + +CreateThread(function() + Config.LoadPlugin('postals', function(pluginConfig) + local lastPostal = nil + local eventPostal = nil + if pluginConfig.enabled then + -- Don't touch this! + function getNearestPostal() + if pluginConfig.mode and pluginConfig.mode == 'event' then + return eventPostal + elseif pluginConfig.mode and pluginConfig.mode == 'file' then + local postalFile = LoadResourceFile(GetCurrentResourceName(), ('/submodules/postals/%s'):format(pluginConfig.customPostalCodesFile)) + if postalFile ~= nil then + local postalData = json.decode(postalFile) + for i, postal in ipairs(postalData) do postalData[i] = { vec(postal.x, postal.y), code = postal.code } end + local coords = GetEntityCoords(PlayerPedId()) + local _nearestIndex, _nearestD + coords = vec(coords[1], coords[2]) + local _total = #postalData + for i = 1, _total do + local D = #(coords - postalData[i][1]) + if not _nearestD or D < _nearestD then + _nearestIndex = i + _nearestD = D + end + end + local _code = postalData[_nearestIndex].code + return _code + else + assert(false, 'Custom postal file not found. Cannot use postals plugin.') + end + else + if exports[pluginConfig.nearestPostalResourceName] ~= nil then + local p = exports[pluginConfig.nearestPostalResourceName]:getPostal() + return p + else + assert(false, 'Required postal resource is not loaded. Cannot use postals plugin.') + end + end + end + if pluginConfig.mode and pluginConfig.nearestPostalEvent and pluginConfig.mode == 'event' then + AddEventHandler(pluginConfig.nearestPostalEvent, function(postal) + eventPostal = postal + end) + end + local function sendPostalData() + local postal = getNearestPostal() + if postal ~= nil and postal ~= lastPostal then + TriggerServerEvent('cadClientPostal', postal) + lastPostal = postal + end + end + CreateThread(function() + while not NetworkIsPlayerActive(PlayerId()) or pluginConfig.sendTimer == nil do + Wait(10) + end + TriggerServerEvent('getShouldSendPostal') + while true do + if pluginConfig.shouldSendPostalData then + sendPostalData() + end + Wait(pluginConfig.sendTimer) + end + end) + RegisterNetEvent('getShouldSendPostalResponse') + AddEventHandler('getShouldSendPostalResponse', function(toggle) + print('got ' .. tostring(toggle)) + pluginConfig.shouldSendPostalData = toggle + end) + end + end) +end) diff --git a/resources/[sonorancad]/sonorancad/submodules/postals/sv_postals.lua b/resources/[sonorancad]/sonorancad/submodules/postals/sv_postals.lua new file mode 100644 index 000000000..e8800cba3 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/postals/sv_postals.lua @@ -0,0 +1,119 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: postals + Creator: SonoranCAD + Description: Fetches nearest postal from client +]] -- Toggles Postal Sender +CreateThread(function() + Config.LoadPlugin('postals', function(pluginConfig) + local locationsConfig = Config.GetPluginConfig('locations') + + if pluginConfig.enabled and locationsConfig ~= nil then + + local postalFile = nil + local postals + local state = GetResourceState(pluginConfig.nearestPostalResourceName) + local shouldStop = false + if pluginConfig.mode and pluginConfig.mode == 'resource' then + if state ~= 'started' then + if state == 'missing' then + logError('POSTAL_RESOURCE_MISSING', getErrorText('POSTAL_RESOURCE_MISSING'):format(pluginConfig.nearestPostalResourceName)) + shouldStop = true + elseif state == 'stopped' then + logError('POSTAL_RESOURCE_STOPPED', getErrorText('POSTAL_RESOURCE_STOPPED'):format(pluginConfig.nearestPostalResourceName, state)) + else + logError('POSTAL_RESOURCE_BAD_STATE', getErrorText('POSTAL_RESOURCE_BAD_STATE'):format(pluginConfig.nearestPostalResourceName, state)) + shouldStop = true + end + else + postalFile = LoadResourceFile(pluginConfig.nearestPostalResourceName, GetResourceMetadata(pluginConfig.nearestPostalResourceName, 'postal_file')) + if postalFile == nil then + logError('POSTAL_CUSTOM_RESOURCE_FILE_ERROR', getErrorText('POSTAL_CUSTOM_RESOURCE_FILE_ERROR'):format(pluginConfig.nearestPostalResourceName, pluginConfig.nearestPostalResourceName)) + end + end + elseif pluginConfig.mode and pluginConfig.mode == 'file' then + postalFile = LoadResourceFile(GetCurrentResourceName(), ('/submodules/postals/%s'):format(pluginConfig.customPostalCodesFile)) + if postalFile == nil then + logError('CUSTOM_POSTALS_FILE_NOT_FOUND', geterrorText('CUSTOM_POSTALS_FILE_NOT_FOUND'):format(pluginConfig.customPostalCodesFile)) + shouldStop = true + end + end + if postalFile == nil then + logError('POSTAL_FILE_READ_ERROR') + shouldStop = true + end + if shouldStop then + pluginConfig.enabled = false + pluginConfig.disableReason = 'postal resource incorrect' + errorLog('Force disabling plugin to prevent client errors.') + return + end + + postals = json.decode(postalFile) + for i, postal in ipairs(postals) do + postals[i] = {vec(postal.x, postal.y), code = postal.code} + end + + PostalsCache = {} + + RegisterNetEvent('getShouldSendPostal') + AddEventHandler('getShouldSendPostal', function() + TriggerClientEvent('getShouldSendPostalResponse', source, locationsConfig.prefixPostal) + end) + + RegisterNetEvent('cadClientPostal') + AddEventHandler('cadClientPostal', function(postal) + PostalsCache[source] = postal + end) + + AddEventHandler('playerDropped', function(player) + PostalsCache[player] = nil + end) + + function getNearestPostal(player) + return PostalsCache[player] + end + + exports('cadGetNearestPostal', getNearestPostal) + + registerApiType('SET_POSTALS', 'general') + + CreateThread(function() + while Config.apiVersion == -1 or postals == nil do + Wait(1000) + end + if Config.apiVersion < 4 or not Config.apiSendEnabled then + return + end + performApiRequest(postalFile, 'SET_POSTALS', function() + end) + end) + + function getPostalFromVector3(coords) + if not coords or postals == nil then + return nil + end + local _total = #postals + local _nearestIndex, _nearestD + coords = vector2(coords.x, coords.y) + + for i = 1, _total do + local D = #(coords - postals[i][1]) + if not _nearestD or D < _nearestD then + _nearestIndex = i + _nearestD = D + end + end + + return postals[_nearestIndex].code + end + + elseif locationsConfig == nil then + errorLog('ERROR: Postals plugin is loaded, but required locations plugin is not. This plugin will not function correctly!') + pluginConfig.enabled = false + pluginConfig.disableReason = 'locations plugin missing' + end + + end) +end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/sonrad/cl_sonrad.lua b/resources/[sonorancad]/sonorancad/submodules/sonrad/cl_sonrad.lua new file mode 100644 index 000000000..bf6984fc3 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/sonrad/cl_sonrad.lua @@ -0,0 +1,20 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: sonrad + Creator: Sonoran Software Systems + Description: Sonoran Radio integration plugin + + Put all client-side logic in this file. +]] + +CreateThread(function() + Config.LoadPlugin("sonrad", function(pluginConfig) + + if pluginConfig.enabled then + + + + end + end) +end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/sonrad/sv_sonrad.lua b/resources/[sonorancad]/sonorancad/submodules/sonrad/sv_sonrad.lua new file mode 100644 index 000000000..025aa94e8 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/sonrad/sv_sonrad.lua @@ -0,0 +1,368 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: sonrad + Creator: Sonoran Software Systems + Description: Sonoran Radio integration plugin + + Put all server-side logic in this file. +]] + +CreateThread(function() Config.LoadPlugin("sonrad", function(pluginConfig) + + if pluginConfig.enabled then + + local CallCache = {} + local UnitCache = {} + local TowerCache = {} + + + if Config.apiVersion > 3 then + -- Register Api Types + registerApiType("ADD_BLIP", "emergency") + registerApiType("MODIFY_BLIP", "emergency") + registerApiType("REMOVE_BLIP", "emergency") + registerApiType("GET_BLIPS", "emergency") + + BlipMan = { + addBlip = function(coords, radius, colorHex, subType, toolTip, icon, dataTable, cb) + local data = {{ + ["serverId"] = GetConvar("sonoran_serverId", 1), + ["blip"] = { + ["id"] = -1, + ["subType"] = subType, + ["coordinates"] = { + ["x"] = coords.x, + ["y"] = coords.y + }, + ["radius"] = radius, + ["icon"] = icon, + ["color"] = colorHex, + ["tooltip"] = toolTip, + ["data"] = dataTable + } + }} + + performApiRequest(data, "ADD_BLIP", function(res) + if cb ~= nil then + cb(res) + end + end) + end, + + addBlips = function(blips, cb) + performApiRequest(blips, "ADD_BLIP", function(res) + if cb ~= nil then + cb(res) + end + end) + end, + + removeBlip = function(ids, cb) + performApiRequest({{ + ["ids"] = ids + }}, "REMOVE_BLIP", function(res) + if cb ~= nil then + cb(res) + end + end) + end, + + modifyBlips = function(dataTable, cb) + performApiRequest(dataTable, "MODIFY_BLIP", function(res) + if cb ~= nil then + cb(res) + end + end) + end, + + getBlips = function(cb) + local data = {{ + ["serverId"] = GetConvar("sonoran_serverId", 1) + }} + performApiRequest(data, "GET_BLIPS", function(res) + if cb ~= nil then + cb(res) + end + end) + end, + + removeWithSubtype = function(subType, cb) + BlipMan.getBlips(function(res) + local dres = json.decode(res) + local ids = {} + for _, v in ipairs(dres) do + if v.subType == subType then + table.insert(ids, #ids + 1, v.id) + end + end + BlipMan.removeBlip(ids, cb) + end) + end, + } + + function GetTower(coords) + for i = 1, #TowerCache do + if TowerCache[i].PropPosition == coords then + return TowerCache[i], i + end + end + return nil, nil + end + function GetTowerFromId(id) + for i, t in ipairs(TowerCache) do + if t.Id == id then + return t, i + end + end + end + function GetTowerCapacity(tower) + if #tower.DishStatus < 1 then + return 1.0 + end + + local n = 0.0 + for i = 1, #tower.DishStatus do + if tower.DishStatus[i] == 'alive' then + n = n + 1.0 + end + end + return n / #tower.DishStatus + end + + RegisterNetEvent("SonoranCAD::sonrad:SyncTowers") + AddEventHandler("SonoranCAD::sonrad:SyncTowers", function(Towers) + BlipMan.removeWithSubtype("repeater", function(res) + debugLog(res) + + TowerCache = Towers + + local BlipQueue = {} + + debugLog(json.encode(TowerCache)) + for _,t in ipairs(TowerCache) do + + if t.NotPhysical then + -- Handling for Mobile Repeaters + title = "Mobile Repeater" + color = "#ff00f6" + status = "MOBILE" + else + -- Handling for Stationary Repeaters + title = "Radio Tower" + color = "#00a6ff" + status = "HEALTHY" + end + + local CurrentBlip = { + ["serverId"] = GetConvar("sonoran_serverId", 1), + ["blip"] = { + ["id"] = -1, + ["subType"] = "repeater", + ["coordinates"] = { + ["x"] = t.PropPosition.x, + ["y"] = t.PropPosition.y + }, + ["radius"] = t.Range * 0.7937, + ["icon"] = "https://sonoransoftware.com/assets/images/icons/email/radio.png", + ["color"] = color, + ["tooltip"] = title, + ["data"] = { + { + ["title"] = "Status", + ["text"] = status, + } + } + } + } + + table.insert(BlipQueue, #BlipQueue + 1, CurrentBlip) + end + + BlipMan.addBlips(BlipQueue, function(res) + local blips = json.decode(res) + for i=1, #TowerCache do + TowerCache[i].BlipID = blips[i].id + end + debugLog("Tower Cache:" .. json.encode(TowerCache)) + end) + end) + end) + + CreateThread(function() + while true do + Wait(5000) + for i=1, #TowerCache do + if TowerCache[i].Modified then + debugLog("Change found during batch... Sending") + TowerCache[i].Modified = false + local color = nil + local status = nil + local title = nil + if TowerCache[i].NotPhysical then + -- Handling for Mobile Repeaters + title = "Mobile Repeater" + color = "#ff00f6" + status = "MOBILE" + else + -- Handling for Stationary Repeaters + title = "Radio Tower" + color = "#00a6ff" + status = "HEALTHY" + end + local data = {{ + ["id"] = TowerCache[i].BlipID, + ["subType"] = "repeater", + ["coordinates"] = { + ["x"] = TowerCache[i].PropPosition.x, + ["y"] = TowerCache[i].PropPosition.y + }, + ["radius"] = TowerCache[i].Range * 0.7937, + ["icon"] = "https://sonoransoftware.com/assets/images/icons/email/radio.png", + ["color"] = color, + ["tooltip"] = title, + ["data"] = { + { + ["title"] = "Health", + ["text"] = status + } + } + }} + BlipMan.modifyBlips(data, function(res) + debugLog(res) + end) + else + --debugLog("No changes during batch... Ignoring") + end + end + end + end) + + RegisterNetEvent("SonoranCAD::sonrad:SyncOneTower") + AddEventHandler("SonoranCAD::sonrad:SyncOneTower", function(towerId, newTower) + local oldTower, towerIndex = GetTowerFromId(towerId) + if not oldTower then + debugLog("Tower not found in cache... Ignoring") + return + end + local BlipID = oldTower.BlipID + if oldTower.PropPosition.x == newTower.PropPosition.x and oldTower.PropPosition.y == newTower.PropPosition.y then + --debugLog("No Changes During Sync... Ignoring" .. towerIndex) + else + debugLog("Changes found during sync... Queuing" .. towerIndex) + TowerCache[towerIndex] = newTower + TowerCache[towerIndex].BlipID = BlipID + TowerCache[towerIndex].Modified = true + end + end) + + RegisterNetEvent("SonoranCAD::sonrad:SetDishStatus") + AddEventHandler("SonoranCAD::sonrad:SetDishStatus", function(towerId, dishStatus) + local tower = GetTowerFromId(towerId) + if not tower then return end + tower.DishStatus = dishStatus + local pct = GetTowerCapacity(tower) + local color = nil + local status = nil + if pct == 1 then + -- Tower is alive and well. + debugLog("TOWER IS HEALTHY") + color = "#00a6ff" + status = "HEALTHY" + elseif pct == 0 then + -- Tower is offline + debugLog("TOWER IS OFFLINE") + color = "#ff0000" + status = "OFFLINE" + else + -- Tower is degraded + debugLog("TOWER IS DEGRADED") + color = "#ff8c00" + status = "DEGRADED" + end + + local data = {{ + ["id"] = tower.BlipID, + ["subType"] = "repeater", + ["coordinates"] = { + ["x"] = tower.PropPosition.x, + ["y"] = tower.PropPosition.y + }, + ["radius"] = tower.Range * 0.7937, + ["icon"] = "https://sonoransoftware.com/assets/images/icons/email/radio.png", + ["color"] = color, + ["tooltip"] = "Radio Tower", + ["data"] = { + { + ["title"] = "Health", + ["text"] = status, + } + } + }} + BlipMan.modifyBlips(data, function(res) + debugLog(res) + end) + end) + else + debugLog("Disabling blip management, API version too low.") + end + + + CreateThread(function() + while true do + Wait(5000) + CallCache = GetCallCache() + UnitCache = GetUnitCache() + for k, v in pairs(CallCache) do + v.dispatch.units = {} + if v.dispatch.idents then + for ka, va in pairs(v.dispatch.idents) do + local unit + local unitId = GetUnitById(va) + table.insert(v.dispatch.units, UnitCache[unitId]) + end + end + end + end + end) + + RegisterNetEvent("SonoranCAD::sonrad:GetCurrentCall") + AddEventHandler("SonoranCAD::sonrad:GetCurrentCall", function() + local playerid = source + local unit = GetUnitByPlayerId(source) + -- print("unit: " .. json.encode(unit)) + for k, v in pairs(CallCache) do + if v.dispatch.idents then + -- print(json.encode(v)) + for ka, va in pairs(v.dispatch.idents) do + -- print("Comparing " .. unit.id .. " to " .. va) + if unit then + if unit.id == va then + TriggerClientEvent("SonoranCAD::sonrad:UpdateCurrentCall", source, v) + -- print("SonoranCAD::sonrad:UpdateCurrentCall " .. source .. " " .. json.encode(v)) + end + end + end + end + end + end) + + RegisterNetEvent("SonoranCAD::sonrad:RadioPanic") + AddEventHandler("SonoranCAD::sonrad:RadioPanic", function() + if not isPluginLoaded("callcommands") then + errorLog("Cannot process radio panic as the required callcommands plugin is not present.") + return + end + sendPanic(source, true) + end) + + RegisterNetEvent("SonoranCAD::sonrad:GetUnitInfo") + AddEventHandler("SonoranCAD::sonrad:GetUnitInfo", function() + local unit = GetUnitByPlayerId(source) + if unit then + TriggerClientEvent("SonoranCAD::sonrad:GetUnitInfo:Return", source, unit) + end + end) + end + +end) end) diff --git a/resources/[sonorancad]/sonorancad/submodules/trafficstop/cl_trafficstop.lua b/resources/[sonorancad]/sonorancad/submodules/trafficstop/cl_trafficstop.lua new file mode 100644 index 000000000..aa5d50d44 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/trafficstop/cl_trafficstop.lua @@ -0,0 +1,10 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: trafficstop + Creator: SonoranCAD + Description: Implements ts command +]] +CreateThread(function() Config.LoadPlugin("dispatchnotify", function(pluginConfig) + +end) end) \ No newline at end of file diff --git a/resources/[sonorancad]/sonorancad/submodules/trafficstop/sv_trafficstop.lua b/resources/[sonorancad]/sonorancad/submodules/trafficstop/sv_trafficstop.lua new file mode 100644 index 000000000..719ea2584 --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/trafficstop/sv_trafficstop.lua @@ -0,0 +1,96 @@ +--[[ + Sonaran CAD Plugins + + Plugin Name: trafficstop + Creator: SonoranCAD + Description: Implements ts command +]] + +CreateThread(function() Config.LoadPlugin("trafficstop", function(pluginConfig) + +if pluginConfig.enabled then + + if pluginConfig.trafficCommand == nil then + pluginConfig.trafficCommand = "ts" + end + + registerApiType("NEW_DISPATCH", "emergency") + + -- Traffic Stop Handler + function HandleTrafficStop(type, source, args, rawCommand) + local identifier = GetIdentifiers(source)[Config.primaryIdentifier] + local index = findIndex(identifier) + local origin = pluginConfig.origin + local status = pluginConfig.status + local priority = pluginConfig.priority + local address = LocationCache[source] ~= nil and LocationCache[source].location or 'Unknown' + local postal = isPluginLoaded("postals") and getNearestPostal(source) or "" + local title = pluginConfig.title + local code = pluginConfig.code + local units = {identifier} + local tempNotes = {} + local notesStr = "" + address = address:gsub('%b[]', '') + -- Checking if there are any description arguments. + if args[1] then + local description = table.concat(args, " ") + if type == "ts" then + description = "Traffic Stop - "..description + if isPluginLoaded("wraithv2") and wraithLastPlates ~= nil then + if wraithLastPlates.locked ~= nil then + local plate = wraithLastPlates.locked.plate:gsub("%s+","") + table.insert(tempNotes, ("PLATE: %s"):format(plate)) + end + end + end + notesStr = table.concat(tempNotes, " ") + local notes = {} + if notesStr ~= "" then + notes = { + { ['time'] = "00:00:00", ['label'] = "Dispatch", ['type'] = "text", ['content'] = notesStr } + } + end + -- Sending the API event + TriggerEvent('SonoranCAD::trafficstop:SendTrafficApi', origin, status, priority, address, postal, title, code, description, units, notes, source) + -- Sending the user a message stating the call has been sent + TriggerClientEvent("chat:addMessage", source, {args = {"^0^5^*[SonoranCAD]^r ", "^7Details regarding you traffic Stop have been added to CAD"}}) + else + -- Throwing an error message due to now call description stated + TriggerClientEvent("chat:addMessage", source, {args = {"^0[ ^1Error ^0] ", "You need to specify Traffic Stop details (IE: vehicle Description)."}}) + end + end + + RegisterCommand(pluginConfig.trafficCommand, function(source, args, rawCommand) + HandleTrafficStop("ts", source, args, rawCommand) + end, pluginConfig.usePermissions) + + -- Client TraficStop request + RegisterServerEvent('SonoranCAD::trafficstop:SendTrafficApi') + AddEventHandler('SonoranCAD::trafficstop:SendTrafficApi', function(origin, status, priority, address, postal, title, code, description, units, notes, source) + -- send an event to be consumed by other resources + TriggerEvent("SonoranCAD::trafficstop:cadIncomingTraffic", origin, status, priority, address, postal, title, code, description, units, notes, source) + if Config.apiSendEnabled then + local data = { + ['serverId'] = Config.serverId, + ['origin'] = origin, + ['status'] = status, + ['priority'] = priority, + ['block'] = "", -- not used, but required + ['postal'] = postal, --TODO + ['address'] = address, + ['title'] = title, + ['code'] = code, + ['description'] = description, + ['units'] = units, + ['notes'] = notes -- required + } + debugLog("sending Traffic Stop!") + performApiRequest({data}, 'NEW_DISPATCH', function() end) + else + debugPrint("[SonoranCAD] API sending is disabled. Traffic Stop ignored.") + end + end) + +end + +end) end) diff --git a/resources/[sonorancad]/sonorancad/submodules/ts3integration/sv_ts3integration.js b/resources/[sonorancad]/sonorancad/submodules/ts3integration/sv_ts3integration.js new file mode 100644 index 000000000..5437d995d --- /dev/null +++ b/resources/[sonorancad]/sonorancad/submodules/ts3integration/sv_ts3integration.js @@ -0,0 +1,184 @@ +/* + SonoranCAD FiveM - A SonoranCAD integration for FiveM servers + Copyright (C) 2020 Sonoran Software + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program in the file "LICENSE". If not, seeSAME
+OPP
+XMIT
+SAME
+OPP
+XMIT
+888
+ +888
+ +FAST
+LOCK
+FAST
+LOCK
+888
+ +888
+ +888
+ +FRONT ANTENNA
+ +REAR ANTENNA
+ + + +Wraith ARS 2X
+FRONT
+REAR
+
+
+
+
+ LOCKED
+LOCKED
+Plate Reader
+UI Settings
+Radar Scale
+1.00x
+Remote Scale
+1.00x
+Reader Scale
+1.00x
+Safezone: 0px
+ +Radar key binds
+ +Hey, it appears this is your first time using the Wraith ARS 2X. Would you like to view the quick start video?
+ + +